Merge branch 'develop' into sdrplay_v3

This commit is contained in:
Jakob Ketterl 2020-05-24 14:05:36 +02:00
commit 385c241858
76 changed files with 2885 additions and 1609 deletions

View File

@ -15,6 +15,13 @@
- Added support for bias tee control on rtl_sdr devices - Added support for bias tee control on rtl_sdr devices
- All connector driven SDRs now support `"rf_gain": "auto"` to enable AGC - All connector driven SDRs now support `"rf_gain": "auto"` to enable AGC
- `rtl_sdr` type now also supports the `direct_sampling` option - `rtl_sdr` type now also supports the `direct_sampling` option
- Added decoding implementation for for digimode "JS8Call"
(requires an installation of [js8call](http://js8call.com/) and
[the js8py library](https://github.com/jketterl/js8py))
- Reorganization of the frontend demodulator code
- Improve receiver load time by concatenating javascript assets
- Docker images migrated to Debian slim images; This was necessary to allow the use of function multiversioning in
csdr and owrx_connector to allow the images to run on a wider range of CPUs
**0.18.0** **0.18.0**
- Support for SoapyRemote - Support for SoapyRemote

View File

@ -8,7 +8,8 @@
"ft8": 1840000, "ft8": 1840000,
"wspr": 1836600, "wspr": 1836600,
"jt65": 1838000, "jt65": 1838000,
"jt9": 1839000 "jt9": 1839000,
"js8": 1842000
} }
}, },
{ {
@ -21,7 +22,8 @@
"wspr": 3592600, "wspr": 3592600,
"jt65": 3570000, "jt65": 3570000,
"jt9": 3572000, "jt9": 3572000,
"ft4": [3568000, 3575000] "ft4": [3568000, 3575000],
"js8": 3578000
} }
}, },
{ {
@ -43,7 +45,8 @@
"wspr": 7038600, "wspr": 7038600,
"jt65": 7076000, "jt65": 7076000,
"jt9": 7078000, "jt9": 7078000,
"ft4": 7047500 "ft4": 7047500,
"js8": 7078000
} }
}, },
{ {
@ -56,7 +59,8 @@
"wspr": 10138700, "wspr": 10138700,
"jt65": 10138000, "jt65": 10138000,
"jt9": 10140000, "jt9": 10140000,
"ft4": 10140000 "ft4": 10140000,
"js8": 10130000
} }
}, },
{ {
@ -69,7 +73,8 @@
"wspr": 14095600, "wspr": 14095600,
"jt65": 14076000, "jt65": 14076000,
"jt9": 14078000, "jt9": 14078000,
"ft4": 14080000 "ft4": 14080000,
"js8": 14078000
} }
}, },
{ {
@ -82,7 +87,8 @@
"wspr": 18104600, "wspr": 18104600,
"jt65": 18102000, "jt65": 18102000,
"jt9": 18104000, "jt9": 18104000,
"ft4": 18104000 "ft4": 18104000,
"js8": 18104000
} }
}, },
{ {
@ -95,7 +101,8 @@
"wspr": 21094600, "wspr": 21094600,
"jt65": 21076000, "jt65": 21076000,
"jt9": 21078000, "jt9": 21078000,
"ft4": 21140000 "ft4": 21140000,
"js8": 21078000
} }
}, },
{ {
@ -108,7 +115,8 @@
"wspr": 24924600, "wspr": 24924600,
"jt65": 24917000, "jt65": 24917000,
"jt9": 24919000, "jt9": 24919000,
"ft4": 24919000 "ft4": 24919000,
"js8": 24922000
} }
}, },
{ {
@ -121,7 +129,8 @@
"wspr": 28124600, "wspr": 28124600,
"jt65": 28076000, "jt65": 28076000,
"jt9": 28078000, "jt9": 28078000,
"ft4": 28180000 "ft4": 28180000,
"js8": 28078000
} }
}, },
{ {
@ -134,7 +143,8 @@
"wspr": 50293000, "wspr": 50293000,
"jt65": 50310000, "jt65": 50310000,
"jt9": 50312000, "jt9": 50312000,
"ft4": 50318000 "ft4": 50318000,
"js8": 50318000
} }
}, },
{ {
@ -189,5 +199,75 @@
"name": "3cm", "name": "3cm",
"lower_bound": 10000000000, "lower_bound": 10000000000,
"upper_bound": 10500000000 "upper_bound": 10500000000
},
{
"name": "120m Broadcast",
"lower_bound": 2300000,
"upper_bound": 2495000
},
{
"name": "90m Broadcast",
"lower_bound": 3200000,
"upper_bound": 3400000
},
{
"name": "75m Broadcast",
"lower_bound": 3900000,
"upper_bound": 4000000
},
{
"name": "60m Broadcast",
"lower_bound": 4750000,
"upper_bound": 4995000
},
{
"name": "49m Broadcast",
"lower_bound": 5900000,
"upper_bound": 6200000
},
{
"name": "41m Broadcast",
"lower_bound": 7200000,
"upper_bound": 7450000
},
{
"name": "31m Broadcast",
"lower_bound": 9400000,
"upper_bound": 9900000
},
{
"name": "25m Broadcast",
"lower_bound": 11600000,
"upper_bound": 12100000
},
{
"name": "22m Broadcast",
"lower_bound": 13570000,
"upper_bound": 13870000
},
{
"name": "19m Broadcast",
"lower_bound": 15100000,
"upper_bound": 15830000
},
{
"name": "16m Broadcast",
"lower_bound": 17480000,
"upper_bound": 17900000
},
{
"name": "15m Broadcast",
"lower_bound": 18900000,
"upper_bound": 19020000
},
{
"name": "13m Broadcast",
"lower_bound": 21450000,
"upper_bound": 21850000
},
{
"name": "11m Broadcast",
"lower_bound": 25670000,
"upper_bound": 26100000
} }
] ]

View File

@ -49,11 +49,13 @@ receiver_asl = 200
receiver_admin = "example@example.com" receiver_admin = "example@example.com"
receiver_gps = {"lat": 47.000000, "lon": 19.000000} receiver_gps = {"lat": 47.000000, "lon": 19.000000}
photo_title = "Panorama of Budapest from Schönherz Zoltán Dormitory" 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 = """ photo_desc = """
You can add your own background photo and receiver information.<br /> You can add your own background photo and receiver information.<br />
Receiver is operated by: <a href="mailto:%[RX_ADMIN]">%[RX_ADMIN]</a><br/> Receiver is operated by: <a href="mailto:openwebrx@localhost" target="_blank">Receiver Operator</a><br/>
Device: %[RX_DEVICE]<br /> Device: Receiver Device<br />
Antenna: %[RX_ANT]<br /> Antenna: Receiver Antenna<br />
Website: <a href="http://localhost" target="_blank">http://localhost</a> Website: <a href="http://localhost" target="_blank">http://localhost</a>
""" """
@ -150,11 +152,11 @@ sdrs = {
"name": "Airspy HF+", "name": "Airspy HF+",
"type": "airspyhf", "type": "airspyhf",
"ppm": 0, "ppm": 0,
"rf_gain": "auto",
"profiles": { "profiles": {
"20m": { "20m": {
"name": "20m", "name": "20m",
"center_freq": 14150000, "center_freq": 14150000,
"rf_gain": 10,
"samp_rate": 768000, "samp_rate": 768000,
"start_freq": 14070000, "start_freq": 14070000,
"start_mod": "usb", "start_mod": "usb",
@ -162,7 +164,6 @@ sdrs = {
"30m": { "30m": {
"name": "30m", "name": "30m",
"center_freq": 10125000, "center_freq": 10125000,
"rf_gain": 10,
"samp_rate": 192000, "samp_rate": 192000,
"start_freq": 10142000, "start_freq": 10142000,
"start_mod": "usb", "start_mod": "usb",
@ -170,7 +171,6 @@ sdrs = {
"40m": { "40m": {
"name": "40m", "name": "40m",
"center_freq": 7100000, "center_freq": 7100000,
"rf_gain": 10,
"samp_rate": 256000, "samp_rate": 256000,
"start_freq": 7070000, "start_freq": 7070000,
"start_mod": "usb", "start_mod": "usb",
@ -178,7 +178,6 @@ sdrs = {
"80m": { "80m": {
"name": "80m", "name": "80m",
"center_freq": 3650000, "center_freq": 3650000,
"rf_gain": 10,
"samp_rate": 768000, "samp_rate": 768000,
"start_freq": 3570000, "start_freq": 3570000,
"start_mod": "usb", "start_mod": "usb",
@ -186,7 +185,6 @@ sdrs = {
"49m": { "49m": {
"name": "49m Broadcast", "name": "49m Broadcast",
"center_freq": 6000000, "center_freq": 6000000,
"rf_gain": 10,
"samp_rate": 768000, "samp_rate": 768000,
"start_freq": 6070000, "start_freq": 6070000,
"start_mod": "am", "start_mod": "am",
@ -285,22 +283,28 @@ google_maps_api_key = ""
# in seconds; default: 2 hours # in seconds; default: 2 hours
map_position_retention_time = 2 * 60 * 60 map_position_retention_time = 2 * 60 * 60
# wsjt decoder queue configuration # decoder queue configuration
# due to the nature of the wsjt operating modes (ft8, ft8, jt9, jt65 and wspr), the data is recorded for a given amount # 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.5 seconds up to 2 minutes) and decoded at the end. this can lead to very high peak loads. # 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. # 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) # the number of workers will limit the total amount of work (one worker will losely occupy one cpu / thread)
wsjt_queue_workers = 2 decoding_queue_workers = 2
# the maximum queue length will cause decodes to be dumped if the workers cannot keep up # 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 # 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 wsjt services running at the same time # i.e. this should be higher than the number of decoding services running at the same time
wsjt_queue_length = 10 decoding_queue_length = 10
# wsjt decoding depth will allow more results, but will also consume more cpu # wsjt decoding depth will allow more results, but will also consume more cpu
wsjt_decoding_depth = 3 wsjt_decoding_depth = 3
# can also be set for each mode separately # can also be set for each mode separately
# jt65 seems to be somewhat prone to erroneous decodes, this setting handles that to some extent # jt65 seems to be somewhat prone to erroneous decodes, this setting handles that to some extent
wsjt_decoding_depths = {"jt65": 1} wsjt_decoding_depths = {"jt65": 1}
# 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
temporary_directory = "/tmp" temporary_directory = "/tmp"
services_enabled = False services_enabled = False
@ -325,4 +329,6 @@ pskreporter_enabled = False
pskreporter_callsign = "N0CALL" pskreporter_callsign = "N0CALL"
# === Web admin settings === # === Web admin settings ===
webadmin_enabled = False # this feature is experimental at the moment. it should not be enabled on shared receivers since it allows remote
# changes to the receiver settings. enable for testing in controlled environment only.
# webadmin_enabled = False

View File

@ -29,7 +29,9 @@ import math
from functools import partial from functools import partial
from owrx.kiss import KissClient, DirewolfConfig from owrx.kiss import KissClient, DirewolfConfig
from owrx.wsjt import Ft8Chopper, WsprChopper, Jt9Chopper, Jt65Chopper, Ft4Chopper from owrx.wsjt import Ft8Profile, WsprProfile, Jt9Profile, Jt65Profile, Ft4Profile
from owrx.js8 import Js8Profiles
from owrx.audio import AudioChopper
import logging import logging
@ -239,7 +241,7 @@ class dsp(object):
if self.fft_compression == "adpcm": if self.fft_compression == "adpcm":
chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"] chain += ["csdr compress_fft_adpcm_f_u8 {fft_size}"]
return chain return chain
chain += ["csdr shift_addition_cc --fifo {shift_pipe}"] chain += ["csdr shift_addfast_cc --fifo {shift_pipe}"]
if self.decimation > 1: if self.decimation > 1:
chain += ["csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING"] chain += ["csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING"]
chain += ["csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING"] chain += ["csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING"]
@ -329,14 +331,14 @@ class dsp(object):
return chain return chain
elif which == "bpsk31" or which == "bpsk63": elif which == "bpsk31" or which == "bpsk63":
return chain + [ return chain + [
"csdr shift_addition_cc --fifo {secondary_shift_pipe}", "csdr shift_addfast_cc --fifo {secondary_shift_pipe}",
"csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff}", "csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff}",
"csdr simple_agc_cc 0.001 0.5", "csdr simple_agc_cc 0.001 0.5",
"csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q", "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q",
"CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8", "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8",
"CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8", "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8",
] ]
elif self.isWsjtMode(which): elif self.isWsjtMode(which) or self.isJs8(which):
chain += ["csdr realpart_cf"] chain += ["csdr realpart_cf"]
if self.last_decimation != 1.0: if self.last_decimation != 1.0:
chain += ["csdr fractional_decimator_ff {last_decimation}"] chain += ["csdr fractional_decimator_ff {last_decimation}"]
@ -449,21 +451,25 @@ class dsp(object):
if self.isWsjtMode(): if self.isWsjtMode():
smd = self.get_secondary_demodulator() smd = self.get_secondary_demodulator()
chopper_cls = None chopper_profile = None
if smd == "ft8": if smd == "ft8":
chopper_cls = Ft8Chopper chopper_profile = Ft8Profile()
elif smd == "wspr": elif smd == "wspr":
chopper_cls = WsprChopper chopper_profile = WsprProfile()
elif smd == "jt65": elif smd == "jt65":
chopper_cls = Jt65Chopper chopper_profile = Jt65Profile()
elif smd == "jt9": elif smd == "jt9":
chopper_cls = Jt9Chopper chopper_profile = Jt9Profile()
elif smd == "ft4": elif smd == "ft4":
chopper_cls = Ft4Chopper chopper_profile = Ft4Profile()
if chopper_cls is not None: if chopper_profile is not None:
chopper = chopper_cls(self, self.secondary_process_demod.stdout) chopper = AudioChopper(self, self.secondary_process_demod.stdout, chopper_profile)
chopper.start() chopper.start()
self.output.send_output("wsjt_demod", chopper.read) self.output.send_output("wsjt_demod", chopper.read)
elif self.isJs8():
chopper = AudioChopper(self, self.secondary_process_demod.stdout, *Js8Profiles.getEnabledProfiles())
chopper.start()
self.output.send_output("js8_demod", chopper.read)
elif self.isPacket(): elif self.isPacket():
# we best get the ax25 packets from the kiss socket # we best get the ax25 packets from the kiss socket
kiss = KissClient(self.direwolf_port) kiss = KissClient(self.direwolf_port)
@ -564,7 +570,7 @@ class dsp(object):
def get_audio_rate(self): def get_audio_rate(self):
if self.isDigitalVoice() or self.isPacket() or self.isPocsag(): if self.isDigitalVoice() or self.isPacket() or self.isPocsag():
return 48000 return 48000
elif self.isWsjtMode(): elif self.isWsjtMode() or self.isJs8():
return 12000 return 12000
return self.get_output_rate() return self.get_output_rate()
@ -578,6 +584,11 @@ class dsp(object):
demodulator = self.get_secondary_demodulator() demodulator = self.get_secondary_demodulator()
return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"] return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4"]
def isJs8(self, demodulator = None):
if demodulator is None:
demodulator = self.get_secondary_demodulator()
return demodulator == "js8"
def isPacket(self, demodulator=None): def isPacket(self, demodulator=None):
if demodulator is None: if demodulator is None:
demodulator = self.get_secondary_demodulator() demodulator = self.get_secondary_demodulator()
@ -596,6 +607,8 @@ class dsp(object):
self.restart() self.restart()
def set_demodulator(self, demodulator): def set_demodulator(self, demodulator):
if demodulator in ["usb", "lsb", "cw"]:
demodulator = "ssb"
if self.demodulator == demodulator: if self.demodulator == demodulator:
return return
self.demodulator = demodulator self.demodulator = demodulator
@ -626,8 +639,7 @@ class dsp(object):
def set_offset_freq(self, offset_freq): def set_offset_freq(self, offset_freq):
self.offset_freq = offset_freq self.offset_freq = offset_freq
if self.running: if self.running:
with self.modification_lock: self.pipes["shift_pipe"].write("%g\n" % (-float(self.offset_freq) / self.samp_rate))
self.pipes["shift_pipe"].write("%g\n" % (-float(self.offset_freq) / self.samp_rate))
def set_center_freq(self, center_freq): def set_center_freq(self, center_freq):
# dsp only needs to know this to be able to pass it to decoders in the form of get_operating_freq() # dsp only needs to know this to be able to pass it to decoders in the form of get_operating_freq()
@ -640,10 +652,9 @@ class dsp(object):
self.low_cut = low_cut self.low_cut = low_cut
self.high_cut = high_cut self.high_cut = high_cut
if self.running: if self.running:
with self.modification_lock: self.pipes["bpf_pipe"].write(
self.pipes["bpf_pipe"].write( "%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate())
"%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate()) )
)
def get_bpf(self): def get_bpf(self):
return [self.low_cut, self.high_cut] return [self.low_cut, self.high_cut]
@ -656,8 +667,7 @@ class dsp(object):
# no squelch required on digital voice modes # no squelch required on digital voice modes
actual_squelch = -150 if self.isDigitalVoice() or self.isPacket() or self.isPocsag() else self.squelch_level actual_squelch = -150 if self.isDigitalVoice() or self.isPacket() or self.isPocsag() else self.squelch_level
if self.running: if self.running:
with self.modification_lock: self.pipes["squelch_pipe"].write("%g\n" % (self.convertToLinear(actual_squelch)))
self.pipes["squelch_pipe"].write("%g\n" % (self.convertToLinear(actual_squelch)))
def set_unvoiced_quality(self, q): def set_unvoiced_quality(self, q):
self.unvoiced_quality = q self.unvoiced_quality = q
@ -730,6 +740,16 @@ class dsp(object):
# create control pipes for csdr # create control pipes for csdr
self.try_create_pipes(self.pipe_names, command_base) self.try_create_pipes(self.pipe_names, command_base)
# send initial config through the pipes
if self.has_pipe("bpf_pipe"):
self.set_bpf(self.low_cut, self.high_cut)
if self.has_pipe("shift_pipe"):
self.set_offset_freq(self.offset_freq)
if self.has_pipe("squelch_pipe"):
self.set_squelch_level(self.squelch_level)
if self.has_pipe("dmr_control_pipe"):
self.set_dmr_filter(3)
# run the command # run the command
command = command_base.format( command = command_base.format(
bpf_pipe=self.pipes["bpf_pipe"], bpf_pipe=self.pipes["bpf_pipe"],
@ -786,16 +806,6 @@ class dsp(object):
self.start_secondary_demodulator() self.start_secondary_demodulator()
# send initial config through the pipes
if self.has_pipe("bpf_pipe"):
self.set_bpf(self.low_cut, self.high_cut)
if self.has_pipe("shift_pipe"):
self.set_offset_freq(self.offset_freq)
if self.has_pipe("squelch_pipe"):
self.set_squelch_level(self.squelch_level)
if self.has_pipe("dmr_control_pipe"):
self.set_dmr_filter(3)
if self.has_pipe("smeter_pipe"): if self.has_pipe("smeter_pipe"):
def read_smeter(): def read_smeter():
raw = self.pipes["smeter_pipe"].readline() raw = self.pipes["smeter_pipe"].readline()

4
debian/changelog vendored
View File

@ -19,6 +19,10 @@ openwebrx (0.19.0) UNRELEASED; urgency=low
* Added support for bias tee control on rtl_sdr devices * Added support for bias tee control on rtl_sdr devices
* All connector driven SDRs now support `"rf_gain": "auto"` to enable AGC * All connector driven SDRs now support `"rf_gain": "auto"` to enable AGC
* `rtl_sdr` type now also supports the `direct_sampling` option * `rtl_sdr` type now also supports the `direct_sampling` option
* Added decoding implementation for for digimode "JS8Call" (requires an
installation of js8call and the js8py library)
* Reorganization of the frontend demodulator code
* Improve receiver load time by concatenating javascript assets
-- Jakob Ketterl <jakob.ketterl@gmx.de> Thu, 20 Feb 2020 21:01:00 +0000 -- Jakob Ketterl <jakob.ketterl@gmx.de> Thu, 20 Feb 2020 21:01:00 +0000

7
debian/control vendored
View File

@ -3,11 +3,14 @@ Maintainer: Jakob Ketterl <jakob.ketterl@gmx.de>
Section: hamradio Section: hamradio
Priority: optional Priority: optional
Standards-Version: 4.2.0 Standards-Version: 4.2.0
Build-Depends: debhelper (>= 10), dh-python, python3 (>= 3.5) Build-Depends: debhelper (>= 11), dh-python, python3-all (>= 3.5), python3-setuptools
Homepage: https://www.openwebrx.de/
Vcs-Browser: https://github.com/jketterl/openwebrx
Vcs-Git: https://github.com/jketterl/openwebrx.git
Package: openwebrx Package: openwebrx
Architecture: all Architecture: all
Depends: adduser, python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.14), netcat, owrx-connector (>= 0.1), ${python3:Depends}, ${misc:Depends} Depends: adduser, python3 (>= 3.5), python3-pkg-resources, csdr (>= 0.14), netcat, owrx-connector (>= 0.2), python3-js8py (>= 0.1), ${python3:Depends}, ${misc:Depends}
Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx, soapysdr-tools Recommends: digiham (>= 0.3), dsd (>= 1.7), sox, direwolf (>= 1.4), wsjtx, soapysdr-tools
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

@ -1,10 +1,6 @@
FROM alpine:3.10 FROM debian:buster-slim
RUN apk add --no-cache bash ADD docker/scripts/js8call-hamlib.patch /
RUN ln -s /usr/local/lib /usr/local/lib64
ADD docker/scripts/direwolf-1.5.patch /
ADD docker/scripts/install-dependencies.sh / ADD docker/scripts/install-dependencies.sh /
RUN /install-dependencies.sh && \ RUN /install-dependencies.sh && \
rm /install-dependencies.sh rm /install-dependencies.sh

View File

@ -1,241 +0,0 @@
diff --git a/Makefile.linux b/Makefile.linux
index 5010833..3f61de9 100644
--- a/Makefile.linux
+++ b/Makefile.linux
@@ -585,102 +585,102 @@ install : $(APPS) direwolf.conf tocalls.txt symbols-new.txt symbolsX.txt dw-icon
# Applications, not installed with package manager, normally go in /usr/local/bin.
# /usr/bin is used instead when installing from .DEB or .RPM package.
#
- $(INSTALL) -D --mode=755 direwolf $(DESTDIR)/bin/direwolf
- $(INSTALL) -D --mode=755 decode_aprs $(DESTDIR)/bin/decode_aprs
- $(INSTALL) -D --mode=755 text2tt $(DESTDIR)/bin/text2tt
- $(INSTALL) -D --mode=755 tt2text $(DESTDIR)/bin/tt2text
- $(INSTALL) -D --mode=755 ll2utm $(DESTDIR)/bin/ll2utm
- $(INSTALL) -D --mode=755 utm2ll $(DESTDIR)/bin/utm2ll
- $(INSTALL) -D --mode=755 aclients $(DESTDIR)/bin/aclients
- $(INSTALL) -D --mode=755 log2gpx $(DESTDIR)/bin/log2gpx
- $(INSTALL) -D --mode=755 gen_packets $(DESTDIR)/bin/gen_packets
- $(INSTALL) -D --mode=755 atest $(DESTDIR)/bin/atest
- $(INSTALL) -D --mode=755 ttcalc $(DESTDIR)/bin/ttcalc
- $(INSTALL) -D --mode=755 kissutil $(DESTDIR)/bin/kissutil
- $(INSTALL) -D --mode=755 cm108 $(DESTDIR)/bin/cm108
- $(INSTALL) -D --mode=755 dwespeak.sh $(DESTDIR)/bin/dwspeak.sh
+ $(INSTALL) -D -m=755 direwolf $(DESTDIR)/bin/direwolf
+ $(INSTALL) -D -m=755 decode_aprs $(DESTDIR)/bin/decode_aprs
+ $(INSTALL) -D -m=755 text2tt $(DESTDIR)/bin/text2tt
+ $(INSTALL) -D -m=755 tt2text $(DESTDIR)/bin/tt2text
+ $(INSTALL) -D -m=755 ll2utm $(DESTDIR)/bin/ll2utm
+ $(INSTALL) -D -m=755 utm2ll $(DESTDIR)/bin/utm2ll
+ $(INSTALL) -D -m=755 aclients $(DESTDIR)/bin/aclients
+ $(INSTALL) -D -m=755 log2gpx $(DESTDIR)/bin/log2gpx
+ $(INSTALL) -D -m=755 gen_packets $(DESTDIR)/bin/gen_packets
+ $(INSTALL) -D -m=755 atest $(DESTDIR)/bin/atest
+ $(INSTALL) -D -m=755 ttcalc $(DESTDIR)/bin/ttcalc
+ $(INSTALL) -D -m=755 kissutil $(DESTDIR)/bin/kissutil
+ $(INSTALL) -D -m=755 cm108 $(DESTDIR)/bin/cm108
+ $(INSTALL) -D -m=755 dwespeak.sh $(DESTDIR)/bin/dwspeak.sh
#
# Telemetry Toolkit executables. Other .conf and .txt files will go into doc directory.
#
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-balloon.pl $(DESTDIR)/bin/telem-balloon.pl
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-bits.pl $(DESTDIR)/bin/telem-bits.pl
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-data.pl $(DESTDIR)/bin/telem-data.pl
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-data91.pl $(DESTDIR)/bin/telem-data91.pl
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-eqns.pl $(DESTDIR)/bin/telem-eqns.pl
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-parm.pl $(DESTDIR)/bin/telem-parm.pl
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-seq.sh $(DESTDIR)/bin/telem-seq.sh
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-unit.pl $(DESTDIR)/bin/telem-unit.pl
- $(INSTALL) -D --mode=755 telemetry-toolkit/telem-volts.py $(DESTDIR)/bin/telem-volts.py
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-balloon.pl $(DESTDIR)/bin/telem-balloon.pl
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-bits.pl $(DESTDIR)/bin/telem-bits.pl
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-data.pl $(DESTDIR)/bin/telem-data.pl
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-data91.pl $(DESTDIR)/bin/telem-data91.pl
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-eqns.pl $(DESTDIR)/bin/telem-eqns.pl
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-parm.pl $(DESTDIR)/bin/telem-parm.pl
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-seq.sh $(DESTDIR)/bin/telem-seq.sh
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-unit.pl $(DESTDIR)/bin/telem-unit.pl
+ $(INSTALL) -D -m=755 telemetry-toolkit/telem-volts.py $(DESTDIR)/bin/telem-volts.py
#
# Misc. data such as "tocall" to system mapping.
#
- $(INSTALL) -D --mode=644 tocalls.txt $(DESTDIR)/share/direwolf/tocalls.txt
- $(INSTALL) -D --mode=644 symbols-new.txt $(DESTDIR)/share/direwolf/symbols-new.txt
- $(INSTALL) -D --mode=644 symbolsX.txt $(DESTDIR)/share/direwolf/symbolsX.txt
+ $(INSTALL) -D -m=644 tocalls.txt $(DESTDIR)/share/direwolf/tocalls.txt
+ $(INSTALL) -D -m=644 symbols-new.txt $(DESTDIR)/share/direwolf/symbols-new.txt
+ $(INSTALL) -D -m=644 symbolsX.txt $(DESTDIR)/share/direwolf/symbolsX.txt
#
# For desktop icon.
#
- $(INSTALL) -D --mode=644 dw-icon.png $(DESTDIR)/share/direwolf/pixmaps/dw-icon.png
- $(INSTALL) -D --mode=644 direwolf.desktop $(DESTDIR)/share/applications/direwolf.desktop
+ $(INSTALL) -D -m=644 dw-icon.png $(DESTDIR)/share/direwolf/pixmaps/dw-icon.png
+ $(INSTALL) -D -m=644 direwolf.desktop $(DESTDIR)/share/applications/direwolf.desktop
#
# Documentation. Various plain text files and PDF.
#
- $(INSTALL) -D --mode=644 CHANGES.md $(DESTDIR)/share/doc/direwolf/CHANGES.md
- $(INSTALL) -D --mode=644 LICENSE-dire-wolf.txt $(DESTDIR)/share/doc/direwolf/LICENSE-dire-wolf.txt
- $(INSTALL) -D --mode=644 LICENSE-other.txt $(DESTDIR)/share/doc/direwolf/LICENSE-other.txt
+ $(INSTALL) -D -m=644 CHANGES.md $(DESTDIR)/share/doc/direwolf/CHANGES.md
+ $(INSTALL) -D -m=644 LICENSE-dire-wolf.txt $(DESTDIR)/share/doc/direwolf/LICENSE-dire-wolf.txt
+ $(INSTALL) -D -m=644 LICENSE-other.txt $(DESTDIR)/share/doc/direwolf/LICENSE-other.txt
#
# ./README.md is an overview for the project main page.
# Maybe we could stick it in some other place.
# doc/README.md contains an overview of the PDF file contents and is more useful here.
#
- $(INSTALL) -D --mode=644 doc/README.md $(DESTDIR)/share/doc/direwolf/README.md
- $(INSTALL) -D --mode=644 doc/2400-4800-PSK-for-APRS-Packet-Radio.pdf $(DESTDIR)/share/doc/direwolf/2400-4800-PSK-for-APRS-Packet-Radio.pdf
- $(INSTALL) -D --mode=644 doc/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf
- $(INSTALL) -D --mode=644 doc/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf
- $(INSTALL) -D --mode=644 doc/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf $(DESTDIR)/share/doc/direwolf/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf
- $(INSTALL) -D --mode=644 doc/APRS-Telemetry-Toolkit.pdf $(DESTDIR)/share/doc/direwolf/APRS-Telemetry-Toolkit.pdf
- $(INSTALL) -D --mode=644 doc/APRStt-Implementation-Notes.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Implementation-Notes.pdf
- $(INSTALL) -D --mode=644 doc/APRStt-interface-for-SARTrack.pdf $(DESTDIR)/share/doc/direwolf/APRStt-interface-for-SARTrack.pdf
- $(INSTALL) -D --mode=644 doc/APRStt-Listening-Example.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Listening-Example.pdf
- $(INSTALL) -D --mode=644 doc/Bluetooth-KISS-TNC.pdf $(DESTDIR)/share/doc/direwolf/Bluetooth-KISS-TNC.pdf
- $(INSTALL) -D --mode=644 doc/Going-beyond-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/Going-beyond-9600-baud.pdf
- $(INSTALL) -D --mode=644 doc/Raspberry-Pi-APRS.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS.pdf
- $(INSTALL) -D --mode=644 doc/Raspberry-Pi-APRS-Tracker.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS-Tracker.pdf
- $(INSTALL) -D --mode=644 doc/Raspberry-Pi-SDR-IGate.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-SDR-IGate.pdf
- $(INSTALL) -D --mode=644 doc/Successful-APRS-IGate-Operation.pdf $(DESTDIR)/share/doc/direwolf/Successful-APRS-IGate-Operation.pdf
- $(INSTALL) -D --mode=644 doc/User-Guide.pdf $(DESTDIR)/share/doc/direwolf/User-Guide.pdf
- $(INSTALL) -D --mode=644 doc/WA8LMF-TNC-Test-CD-Results.pdf $(DESTDIR)/share/doc/direwolf/WA8LMF-TNC-Test-CD-Results.pdf
+ $(INSTALL) -D -m=644 doc/README.md $(DESTDIR)/share/doc/direwolf/README.md
+ $(INSTALL) -D -m=644 doc/2400-4800-PSK-for-APRS-Packet-Radio.pdf $(DESTDIR)/share/doc/direwolf/2400-4800-PSK-for-APRS-Packet-Radio.pdf
+ $(INSTALL) -D -m=644 doc/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-1-1200-baud.pdf
+ $(INSTALL) -D -m=644 doc/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/A-Better-APRS-Packet-Demodulator-Part-2-9600-baud.pdf
+ $(INSTALL) -D -m=644 doc/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf $(DESTDIR)/share/doc/direwolf/A-Closer-Look-at-the-WA8LMF-TNC-Test-CD.pdf
+ $(INSTALL) -D -m=644 doc/APRS-Telemetry-Toolkit.pdf $(DESTDIR)/share/doc/direwolf/APRS-Telemetry-Toolkit.pdf
+ $(INSTALL) -D -m=644 doc/APRStt-Implementation-Notes.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Implementation-Notes.pdf
+ $(INSTALL) -D -m=644 doc/APRStt-interface-for-SARTrack.pdf $(DESTDIR)/share/doc/direwolf/APRStt-interface-for-SARTrack.pdf
+ $(INSTALL) -D -m=644 doc/APRStt-Listening-Example.pdf $(DESTDIR)/share/doc/direwolf/APRStt-Listening-Example.pdf
+ $(INSTALL) -D -m=644 doc/Bluetooth-KISS-TNC.pdf $(DESTDIR)/share/doc/direwolf/Bluetooth-KISS-TNC.pdf
+ $(INSTALL) -D -m=644 doc/Going-beyond-9600-baud.pdf $(DESTDIR)/share/doc/direwolf/Going-beyond-9600-baud.pdf
+ $(INSTALL) -D -m=644 doc/Raspberry-Pi-APRS.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS.pdf
+ $(INSTALL) -D -m=644 doc/Raspberry-Pi-APRS-Tracker.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-APRS-Tracker.pdf
+ $(INSTALL) -D -m=644 doc/Raspberry-Pi-SDR-IGate.pdf $(DESTDIR)/share/doc/direwolf/Raspberry-Pi-SDR-IGate.pdf
+ $(INSTALL) -D -m=644 doc/Successful-APRS-IGate-Operation.pdf $(DESTDIR)/share/doc/direwolf/Successful-APRS-IGate-Operation.pdf
+ $(INSTALL) -D -m=644 doc/User-Guide.pdf $(DESTDIR)/share/doc/direwolf/User-Guide.pdf
+ $(INSTALL) -D -m=644 doc/WA8LMF-TNC-Test-CD-Results.pdf $(DESTDIR)/share/doc/direwolf/WA8LMF-TNC-Test-CD-Results.pdf
#
# Various sample config and other files go into examples under the doc directory.
# When building from source, these can be put in home directory with "make install-conf".
# When installed from .DEB or .RPM package, the user will need to copy these to
# the home directory or other desired location.
#
- $(INSTALL) -D --mode=644 direwolf.conf $(DESTDIR)/share/doc/direwolf/examples/direwolf.conf
- $(INSTALL) -D --mode=755 dw-start.sh $(DESTDIR)/share/doc/direwolf/examples/dw-start.sh
- $(INSTALL) -D --mode=644 sdr.conf $(DESTDIR)/share/doc/direwolf/examples/sdr.conf
- $(INSTALL) -D --mode=644 telemetry-toolkit/telem-m0xer-3.txt $(DESTDIR)/share/doc/direwolf/examples/telem-m0xer-3.txt
- $(INSTALL) -D --mode=644 telemetry-toolkit/telem-balloon.conf $(DESTDIR)/share/doc/direwolf/examples/telem-balloon.conf
- $(INSTALL) -D --mode=644 telemetry-toolkit/telem-volts.conf $(DESTDIR)/share/doc/direwolf/examples/telem-volts.conf
+ $(INSTALL) -D -m=644 direwolf.conf $(DESTDIR)/share/doc/direwolf/examples/direwolf.conf
+ $(INSTALL) -D -m=755 dw-start.sh $(DESTDIR)/share/doc/direwolf/examples/dw-start.sh
+ $(INSTALL) -D -m=644 sdr.conf $(DESTDIR)/share/doc/direwolf/examples/sdr.conf
+ $(INSTALL) -D -m=644 telemetry-toolkit/telem-m0xer-3.txt $(DESTDIR)/share/doc/direwolf/examples/telem-m0xer-3.txt
+ $(INSTALL) -D -m=644 telemetry-toolkit/telem-balloon.conf $(DESTDIR)/share/doc/direwolf/examples/telem-balloon.conf
+ $(INSTALL) -D -m=644 telemetry-toolkit/telem-volts.conf $(DESTDIR)/share/doc/direwolf/examples/telem-volts.conf
#
# "man" pages
#
- $(INSTALL) -D --mode=644 man1/aclients.1 $(DESTDIR)/share/man/man1/aclients.1
- $(INSTALL) -D --mode=644 man1/atest.1 $(DESTDIR)/share/man/man1/atest.1
- $(INSTALL) -D --mode=644 man1/decode_aprs.1 $(DESTDIR)/share/man/man1/decode_aprs.1
- $(INSTALL) -D --mode=644 man1/direwolf.1 $(DESTDIR)/share/man/man1/direwolf.1
- $(INSTALL) -D --mode=644 man1/gen_packets.1 $(DESTDIR)/share/man/man1/gen_packets.1
- $(INSTALL) -D --mode=644 man1/kissutil.1 $(DESTDIR)/share/man/man1/kissutil.1
- $(INSTALL) -D --mode=644 man1/ll2utm.1 $(DESTDIR)/share/man/man1/ll2utm.1
- $(INSTALL) -D --mode=644 man1/log2gpx.1 $(DESTDIR)/share/man/man1/log2gpx.1
- $(INSTALL) -D --mode=644 man1/text2tt.1 $(DESTDIR)/share/man/man1/text2tt.1
- $(INSTALL) -D --mode=644 man1/tt2text.1 $(DESTDIR)/share/man/man1/tt2text.1
- $(INSTALL) -D --mode=644 man1/utm2ll.1 $(DESTDIR)/share/man/man1/utm2ll.1
+ $(INSTALL) -D -m=644 man1/aclients.1 $(DESTDIR)/share/man/man1/aclients.1
+ $(INSTALL) -D -m=644 man1/atest.1 $(DESTDIR)/share/man/man1/atest.1
+ $(INSTALL) -D -m=644 man1/decode_aprs.1 $(DESTDIR)/share/man/man1/decode_aprs.1
+ $(INSTALL) -D -m=644 man1/direwolf.1 $(DESTDIR)/share/man/man1/direwolf.1
+ $(INSTALL) -D -m=644 man1/gen_packets.1 $(DESTDIR)/share/man/man1/gen_packets.1
+ $(INSTALL) -D -m=644 man1/kissutil.1 $(DESTDIR)/share/man/man1/kissutil.1
+ $(INSTALL) -D -m=644 man1/ll2utm.1 $(DESTDIR)/share/man/man1/ll2utm.1
+ $(INSTALL) -D -m=644 man1/log2gpx.1 $(DESTDIR)/share/man/man1/log2gpx.1
+ $(INSTALL) -D -m=644 man1/text2tt.1 $(DESTDIR)/share/man/man1/text2tt.1
+ $(INSTALL) -D -m=644 man1/tt2text.1 $(DESTDIR)/share/man/man1/tt2text.1
+ $(INSTALL) -D -m=644 man1/utm2ll.1 $(DESTDIR)/share/man/man1/utm2ll.1
#
# Set group and mode of HID devices corresponding to C-Media USB Audio adapters.
# This will allow us to use the CM108/CM119 GPIO pins for PTT.
#
- $(INSTALL) -D --mode=644 99-direwolf-cmedia.rules /etc/udev/rules.d/99-direwolf-cmedia.rules
+ $(INSTALL) -D -m=644 99-direwolf-cmedia.rules /etc/udev/rules.d/99-direwolf-cmedia.rules
#
@echo " "
@echo "If this is your first install, not an upgrade, type this to put a copy"
diff --git a/cdigipeater.c b/cdigipeater.c
index 9c40d95..94112e9 100644
--- a/cdigipeater.c
+++ b/cdigipeater.c
@@ -49,7 +49,7 @@
#include <stdio.h>
#include <ctype.h> /* for isdigit, isupper */
#include "regex.h"
-#include <sys/unistd.h>
+#include <unistd.h>
#include "ax25_pad.h"
#include "cdigipeater.h"
diff --git a/decode_aprs.c b/decode_aprs.c
index 35c186b..a620cb3 100644
--- a/decode_aprs.c
+++ b/decode_aprs.c
@@ -3872,11 +3872,7 @@ static void decode_tocall (decode_aprs_t *A, char *dest)
* models before getting to the more generic APY.
*/
-#if defined(__WIN32__) || defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__APPLE__)
qsort (tocalls, num_tocalls, sizeof(struct tocalls_s), tocall_cmp);
-#else
- qsort (tocalls, num_tocalls, sizeof(struct tocalls_s), (__compar_fn_t)tocall_cmp);
-#endif
}
else {
if ( ! A->g_quiet) {
diff --git a/digipeater.c b/digipeater.c
index 36970d7..5195582 100644
--- a/digipeater.c
+++ b/digipeater.c
@@ -62,7 +62,7 @@
#include <stdio.h>
#include <ctype.h> /* for isdigit, isupper */
#include "regex.h"
-#include <sys/unistd.h>
+#include <unistd.h>
#include "ax25_pad.h"
#include "digipeater.h"
diff --git a/direwolf.h b/direwolf.h
index 514bcc5..52f5ae9 100644
--- a/direwolf.h
+++ b/direwolf.h
@@ -274,7 +274,7 @@ char *strtok_r(char *str, const char *delim, char **saveptr);
char *strcasestr(const char *S, const char *FIND);
-#if defined(__OpenBSD__) || defined(__FreeBSD__) || defined(__APPLE__)
+#if 1
// strlcpy and strlcat should be in string.h and the C library.
diff --git a/multi_modem.c b/multi_modem.c
index 5d96c79..24261b9 100644
--- a/multi_modem.c
+++ b/multi_modem.c
@@ -80,7 +80,7 @@
#include <string.h>
#include <assert.h>
#include <stdio.h>
-#include <sys/unistd.h>
+#include <unistd.h>
#include "ax25_pad.h"
#include "textcolor.h"

View File

@ -18,12 +18,14 @@ function cmakebuild() {
cd /tmp cd /tmp
BUILD_PACKAGES="git cmake make gcc g++ musl-dev" BUILD_PACKAGES="git cmake make gcc g++"
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES
apt-get update
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 9d72cf1382ed90735632a6d0ef6f820a4758f733 cmakebuild owrx_connector 4cb8d14fbe387b1569a5b635d7819266ce1dd42b
apk del .build-deps apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -18,11 +18,11 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libusb" STATIC_PACKAGES="libusb-1.0-0"
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers" BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/airspy/airspyone_host.git git clone https://github.com/airspy/airspyone_host.git
cmakebuild airspyone_host bceca18f9e3a5f89cff78c4d949c71771d92dfd3 cmakebuild airspyone_host bceca18f9e3a5f89cff78c4d949c71771d92dfd3
@ -36,4 +36,6 @@ cmakebuild airspyhf 613852a2bb64af42690bf9be2201826af69a9475
git clone https://github.com/pothosware/SoapyAirspyHF.git git clone https://github.com/pothosware/SoapyAirspyHF.git
cmakebuild SoapyAirspyHF 81ca737bb044dd930a9de738bced1e4915491f1b cmakebuild SoapyAirspyHF 81ca737bb044dd930a9de738bced1e4915491f1b
apk del .build-deps apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -18,11 +18,11 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libusb fftw udev" STATIC_PACKAGES="libusb-1.0-0 libfftw3-3 udev"
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev fftw-dev" BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev libfftw3-dev pkg-config"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/mossmann/hackrf.git git clone https://github.com/mossmann/hackrf.git
cd hackrf cd hackrf
@ -31,4 +31,6 @@ cmakebuild host
cd .. cd ..
rm -rf hackrf rm -rf hackrf
apk del .build-deps SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -4,11 +4,11 @@ export MAKEFLAGS="-j4"
cd /tmp cd /tmp
STATIC_PACKAGES="libusb" STATIC_PACKAGES="libusb-1.0-0 libatomic1"
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers" BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/myriadrf/LimeSuite.git git clone https://github.com/myriadrf/LimeSuite.git
cd LimeSuite cd LimeSuite
@ -21,4 +21,6 @@ make install
cd ../.. cd ../..
rm -rf LimeSuite rm -rf LimeSuite
apk del .build-deps apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -4,11 +4,11 @@ export MAKEFLAGS="-j4"
cd /tmp cd /tmp
STATIC_PACKAGES="libusb udev" STATIC_PACKAGES="libusb-1.0-0 libudev1"
BUILD_PACKAGES="git make gcc autoconf automake libtool musl-dev libusb-dev shadow vim" BUILD_PACKAGES="git make gcc autoconf automake libtool libusb-1.0-0-dev xxd"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/Microtelecom/libperseus-sdr.git git clone https://github.com/Microtelecom/libperseus-sdr.git
cd libperseus-sdr cd libperseus-sdr
@ -21,4 +21,6 @@ ldconfig /etc/ld.so.conf.d
cd .. cd ..
rm -rf libperseus-sdr rm -rf libperseus-sdr
apk del .build-deps apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -18,11 +18,11 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libusb libxml2" STATIC_PACKAGES="libusb-1.0-0 libxml2"
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers libxml2-dev flex bison" BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ libxml2-dev flex bison"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/analogdevicesinc/libiio.git git clone https://github.com/analogdevicesinc/libiio.git
cmakebuild libiio 5f5af2e417129ad8f4e05fc5c1b730f0694dca12 -DCMAKE_INSTALL_PREFIX=/usr/local cmakebuild libiio 5f5af2e417129ad8f4e05fc5c1b730f0694dca12 -DCMAKE_INSTALL_PREFIX=/usr/local
@ -33,4 +33,6 @@ cmakebuild libad9361-iio 8ac95f3325c18c2e34cd9cfd49c7b63d69a0a9d2
git clone https://github.com/pothosware/SoapyPlutoSDR.git git clone https://github.com/pothosware/SoapyPlutoSDR.git
cmakebuild SoapyPlutoSDR c88b7f5bac1e5785f212f9a7c6ce8fef64eb719e cmakebuild SoapyPlutoSDR c88b7f5bac1e5785f212f9a7c6ce8fef64eb719e
apk del .build-deps apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -18,11 +18,11 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libusb" STATIC_PACKAGES="libusb-1.0-0"
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers" BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/osmocom/rtl-sdr.git git clone https://github.com/osmocom/rtl-sdr.git
cmakebuild rtl-sdr d794155ba65796a76cd0a436f9709f4601509320 cmakebuild rtl-sdr d794155ba65796a76cd0a436f9709f4601509320
@ -30,4 +30,6 @@ cmakebuild rtl-sdr d794155ba65796a76cd0a436f9709f4601509320
git clone https://github.com/pothosware/SoapyRTLSDR.git git clone https://github.com/pothosware/SoapyRTLSDR.git
cmakebuild SoapyRTLSDR 8ba18f17d64005e43ff2a4e46611f8c710b05007 cmakebuild SoapyRTLSDR 8ba18f17d64005e43ff2a4e46611f8c710b05007
apk del .build-deps apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -18,13 +18,15 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libusb" STATIC_PACKAGES="libusb-1.0.0"
BUILD_PACKAGES="git libusb-dev cmake make gcc musl-dev g++ linux-headers" BUILD_PACKAGES="git libusb-1.0.0-dev cmake make gcc g++ pkg-config"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/osmocom/rtl-sdr.git git clone https://github.com/osmocom/rtl-sdr.git
cmakebuild rtl-sdr d794155ba65796a76cd0a436f9709f4601509320 cmakebuild rtl-sdr d794155ba65796a76cd0a436f9709f4601509320
apk del .build-deps apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -18,11 +18,11 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="libusb udev" STATIC_PACKAGES="libusb-1.0.0 libudev1"
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev" BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-dev"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
ARCH=$(uname -m) ARCH=$(uname -m)
@ -51,4 +51,6 @@ rm $BINARY
git clone https://github.com/fventuri/SoapySDRPlay.git git clone https://github.com/fventuri/SoapySDRPlay.git
cmakebuild SoapySDRPlay 9746de21d5a3778c444cc5e70da2a61c27cb614a cmakebuild SoapySDRPlay 9746de21d5a3778c444cc5e70da2a61c27cb614a
apk del .build-deps SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -18,13 +18,15 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="avahi" STATIC_PACKAGES="avahi-daemon libavahi-client3"
BUILD_PACKAGES="git cmake make gcc musl-dev g++ linux-headers avahi-dev" BUILD_PACKAGES="git cmake make gcc g++ libavahi-client-dev"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/pothosware/SoapyRemote.git git clone https://github.com/pothosware/SoapyRemote.git
cmakebuild SoapyRemote 6d9bd820da470cfe7b27b2e6946af93cfece448f cmakebuild SoapyRemote 6d9bd820da470cfe7b27b2e6946af93cfece448f
apk del .build-deps apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -18,13 +18,15 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="udev" STATIC_PACKAGES="libudev1"
BUILD_PACKAGES="git cmake make patch wget sudo gcc g++" BUILD_PACKAGES="git cmake make patch wget sudo gcc g++"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/pothosware/SoapySDR git clone https://github.com/pothosware/SoapySDR
cmakebuild SoapySDR f722f9ce5b629c3c44401a9bf628b3f8e67a9695 cmakebuild SoapySDR f722f9ce5b629c3c44401a9bf628b3f8e67a9695
apk del .build-deps SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -9,7 +9,7 @@ function cmakebuild() {
fi fi
mkdir build mkdir build
cd build cd build
cmake .. cmake ${CMAKE_ARGS:-} ..
make make
make install make install
cd ../.. cd ../..
@ -18,18 +18,27 @@ function cmakebuild() {
cd /tmp cd /tmp
STATIC_PACKAGES="sox fftw python3 netcat-openbsd libsndfile lapack libusb qt5-qtbase qt5-qtmultimedia qt5-qtserialport qt5-qttools alsa-lib" STATIC_PACKAGES="sox libfftw3-bin python3 python3-setuptools netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5serialport5 libqt5multimedia5-plugins libqt5widgets5 libqt5sql5-sqlite libqt5core5a libqt5multimedia5 libqt5network5 libqt5printsupport5 libqt5serialport5 libqt5sql5 libqt5widgets5 libreadline7 libgfortran4 libgomp1 libasound2 libudev1 libhamlib2 ca-certificates"
BUILD_PACKAGES="git libsndfile-dev fftw-dev cmake ca-certificates make gcc musl-dev g++ lapack-dev linux-headers autoconf automake libtool texinfo gfortran libusb-dev qt5-qtbase-dev qt5-qtmultimedia-dev qt5-qtserialport-dev qt5-qttools-dev asciidoctor asciidoc alsa-lib-dev linux-headers" BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev autoconf automake libtool texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev pkg-config libudev-dev libhamlib-dev patch xsltproc"
apk add --no-cache $STATIC_PACKAGES apt-get update
apk add --no-cache --virtual .build-deps $BUILD_PACKAGES apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES
git clone https://github.com/jketterl/js8py.git
pushd js8py
git checkout 888e62be375316882ad2b2ac8e396c3bf857b6fc
python3 setup.py install
popd
rm -rf js8py
git clone https://git.code.sf.net/p/itpp/git itpp git clone https://git.code.sf.net/p/itpp/git itpp
cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d cmakebuild itpp bb5c7e95f40e8fdb5c3f3d01a84bcbaf76f3676d
git clone https://github.com/jketterl/csdr.git git clone https://github.com/jketterl/csdr.git
cd csdr cd csdr
git checkout fe0b042d9cdc2605a817ca7fdd3a23c48bf07563 git checkout 69c4d74a5b8207b0edf4a36a5a0795fbee39281f
autoreconf -i
./configure
make make
make install make install
cd .. cd ..
@ -44,16 +53,26 @@ cmakebuild digiham 95206501be89b38d0267bf6c29a6898e7c65656f
git clone https://github.com/f4exb/dsd.git git clone https://github.com/f4exb/dsd.git
cmakebuild dsd f6939f9edbbc6f66261833616391a4e59cb2b3d7 cmakebuild dsd f6939f9edbbc6f66261833616391a4e59cb2b3d7
JS8CALL_VERSION=2.1.1
JS8CALL_DIR=js8call-${JS8CALL_VERSION}
JS8CALL_TGZ=${JS8CALL_DIR}.tgz
wget http://files.js8call.com/${JS8CALL_VERSION}/${JS8CALL_TGZ}
tar xfz ${JS8CALL_TGZ}
# patch allows us to build against the packaged hamlib
patch -Np1 -d ${JS8CALL_DIR} < /js8call-hamlib.patch
rm /js8call-hamlib.patch
CMAKE_ARGS="-D CMAKE_CXX_FLAGS=-DJS8_USE_LEGACY_HAMLIB" cmakebuild ${JS8CALL_DIR}
rm ${JS8CALL_TGZ}
WSJT_DIR=wsjtx-2.1.2 WSJT_DIR=wsjtx-2.1.2
WSJT_TGZ=${WSJT_DIR}.tgz WSJT_TGZ=${WSJT_DIR}.tgz
wget http://physics.princeton.edu/pulsar/k1jt/$WSJT_TGZ wget http://physics.princeton.edu/pulsar/k1jt/${WSJT_TGZ}
tar xvfz $WSJT_TGZ tar xfz ${WSJT_TGZ}
cmakebuild $WSJT_DIR cmakebuild ${WSJT_DIR}
rm $WSJT_TGZ rm ${WSJT_TGZ}
git clone --depth 1 -b 1.5 https://github.com/wb2osz/direwolf.git git clone --depth 1 -b 1.5 https://github.com/wb2osz/direwolf.git
cd direwolf cd direwolf
patch -Np1 < /direwolf-1.5.patch
make make
make install make install
cd .. cd ..
@ -64,4 +83,6 @@ pushd /opt/aprs-symbols
git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802 git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802
popd popd
apk del .build-deps apt-get -y purge --autoremove $BUILD_PACKAGES
apt-get clean
rm -rf /var/lib/apt/lists/*

View File

@ -0,0 +1,43 @@
diff -ur js8call-2.1.1-orig/CMake/Modules/Findhamlib.cmake js8call-2.1.1/CMake/Modules/Findhamlib.cmake
--- js8call-2.1.1-orig/CMake/Modules/Findhamlib.cmake 2020-05-23 15:38:20.730349612 +0000
+++ js8call-2.1.1/CMake/Modules/Findhamlib.cmake 2020-05-23 15:39:28.829772207 +0000
@@ -78,4 +78,4 @@
# Handle the QUIETLY and REQUIRED arguments and set HAMLIB_FOUND to
# TRUE if all listed variables are TRUE
include (FindPackageHandleStandardArgs)
-find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES hamlib_LIBRARY_DIRS)
+find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES)
diff -ur js8call-2.1.1-orig/CMakeLists.txt js8call-2.1.1/CMakeLists.txt
--- js8call-2.1.1-orig/CMakeLists.txt 2020-05-23 15:38:20.730349612 +0000
+++ js8call-2.1.1/CMakeLists.txt 2020-05-23 15:52:46.103389553 +0000
@@ -683,7 +683,7 @@
#
# libhamlib setup
#
-set (hamlib_STATIC 1)
+set (hamlib_STATIC 0)
find_package (hamlib 3 REQUIRED)
find_program (RIGCTL_EXE rigctl)
find_program (RIGCTLD_EXE rigctld)
@@ -1106,20 +1106,6 @@
BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
)
-install (PROGRAMS
- ${RIGCTL_EXE}
- DESTINATION ${CMAKE_INSTALL_BINDIR}
- #COMPONENT runtime
- RENAME rigctl-local${CMAKE_EXECUTABLE_SUFFIX}
- )
-
-install (PROGRAMS
- ${RIGCTLD_EXE}
- DESTINATION ${CMAKE_INSTALL_BINDIR}
- #COMPONENT runtime
- RENAME rigctld-local${CMAKE_EXECUTABLE_SUFFIX}
- )
-
install (FILES
README
COPYING
Only in js8call-2.1.1/: hamlib.patch

View File

@ -1,16 +1,6 @@
@import url("openwebrx-header.css"); @import url("openwebrx-header.css");
@import url("openwebrx-globals.css"); @import url("openwebrx-globals.css");
/* expandable photo not implemented in admin area page */
#webrx-top-photo-clip {
max-height: 67px;
}
body {
background-color: #2e2e2e;
color: #DDD;
}
.buttons { .buttons {
text-align: right; text-align: right;
} }
@ -18,3 +8,7 @@ body {
.row .map-input { .row .map-input {
margin: 15px 15px 0; margin: 15px 15px 0;
} }
.device {
margin-top: 20px;
}

12
htdocs/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,7 @@
@import url("openwebrx-header.css"); @import url("openwebrx-header.css");
@import url("openwebrx-globals.css"); @import url("openwebrx-globals.css");
/* expandable photo not implemented on features page */
#webrx-top-photo-clip {
max-height: 67px;
}
h1 { h1 {
text-align: center; text-align: center;
margin: 50px 0; margin: 50px 0;
} }

View File

@ -1,16 +1,6 @@
@import url("openwebrx-header.css"); @import url("openwebrx-header.css");
@import url("openwebrx-globals.css"); @import url("openwebrx-globals.css");
/* expandable photo not implemented on login page */
#webrx-top-photo-clip {
max-height: 67px;
}
body {
background-color: #2e2e2e;
}
.login { .login {
position: absolute; position: absolute;
left: 50%; left: 50%;
@ -19,7 +9,6 @@ body {
width: 500px; width: 500px;
background-color: #ddd;
padding: 20px; padding: 20px;
border-radius: 10px; border-radius: 10px;
border: 1px solid #575757; border: 1px solid #575757;
@ -31,8 +20,5 @@ body {
} }
.btn-login { .btn-login {
color: #FFF;
background-color: #2e2e2e;
border-color: #2e2e2e;
height: 50px; height: 50px;
} }

View File

@ -1,11 +1,6 @@
@import url("openwebrx-header.css"); @import url("openwebrx-header.css");
@import url("openwebrx-globals.css"); @import url("openwebrx-globals.css");
/* expandable photo not implemented on map page */
#webrx-top-photo-clip {
max-height: 67px;
}
body { body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -2,6 +2,7 @@
{ {
position: relative; position: relative;
z-index:1000; z-index:1000;
background-color: #575757;
} }
#webrx-top-photo #webrx-top-photo
@ -13,7 +14,8 @@
#webrx-top-photo-clip #webrx-top-photo-clip
{ {
min-height: 67px; min-height: 67px;
max-height: 350px; max-height: 67px;
height: 350px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
} }
@ -41,18 +43,21 @@
right: 0; right: 0;
} }
#webrx-tob-container, #webrx-top-container * {
line-height: initial;
box-sizing: initial;
}
#webrx-top-container img {
vertical-align: initial;
}
#webrx-top-logo #webrx-top-logo
{ {
padding: 12px; padding: 12px;
float: left; float: left;
} }
#webrx-ha5kfu-top-logo
{
float: right;
padding: 15px;
}
#webrx-rx-avatar #webrx-rx-avatar
{ {
background-color: rgba(154, 154, 154, .5); background-color: rgba(154, 154, 154, .5);
@ -107,18 +112,15 @@
cursor:pointer; cursor:pointer;
position: absolute; position: absolute;
left: 470px; left: 470px;
top: 51px; top: 55px;
} }
#openwebrx-rx-details-arrow a #openwebrx-rx-details-arrow a
{ {
margin: 0; margin: 0;
padding: 0; padding: 0;
} line-height: 0;
display: block;
#openwebrx-rx-details-arrow-down
{
display:none;
} }
#openwebrx-main-buttons .button { #openwebrx-main-buttons .button {
@ -149,7 +151,7 @@
#openwebrx-main-buttons #openwebrx-main-buttons
{ {
padding: 5px 0; padding: 5px 15px;
display: flex; display: flex;
list-style: none; list-style: none;
float: right; float: right;

View File

@ -150,6 +150,10 @@ input[type=range]:focus::-ms-fill-upper
background: #B6B6B6; background: #B6B6B6;
} }
input[type=range]:disabled {
opacity: 0.5;
}
#webrx-page-container #webrx-page-container
{ {
height: 100%; height: 100%;
@ -311,7 +315,7 @@ input[type=range]:focus::-ms-fill-upper
font-style: normal; font-style: normal;
} }
#webrx-actual-freq { .webrx-actual-freq {
width: 100%; width: 100%;
text-align: left; text-align: left;
padding: 0; padding: 0;
@ -320,11 +324,11 @@ input[type=range]:focus::-ms-fill-upper
flex-direction: row; flex-direction: row;
} }
#webrx-actual-freq > * { .webrx-actual-freq > * {
flex: 1; flex: 1;
} }
#webrx-actual-freq input { .webrx-actual-freq input {
font-family: 'roboto-mono'; font-family: 'roboto-mono';
width: 0; width: 0;
box-sizing: border-box; box-sizing: border-box;
@ -334,14 +338,13 @@ input[type=range]:focus::-ms-fill-upper
color: inherit; color: inherit;
} }
#webrx-actual-freq, #webrx-actual-freq input { .webrx-actual-freq, .webrx-actual-freq input {
font-size: 16pt; font-size: 16pt;
font-family: 'roboto-mono'; font-family: 'roboto-mono';
line-height: 22px; line-height: 22px;
} }
#webrx-mouse-freq .webrx-mouse-freq {
{
width: 100%; width: 100%;
text-align: left; text-align: left;
font-size: 10pt; font-size: 10pt;
@ -381,6 +384,7 @@ input[type=range]:focus::-ms-fill-upper
border-radius: 15px; border-radius: 15px;
-moz-border-radius: 15px; -moz-border-radius: 15px;
margin: 5.9px; margin: 5.9px;
box-sizing: content-box;
} }
.openwebrx-panel a .openwebrx-panel a
@ -435,6 +439,10 @@ input[type=range]:focus::-ms-fill-upper
margin-right: 0; margin-right: 0;
} }
.openwebrx-button.disabled {
opacity: 0.5;
}
.openwebrx-demodulator-button .openwebrx-demodulator-button
{ {
width: 38px; width: 38px;
@ -445,6 +453,10 @@ input[type=range]:focus::-ms-fill-upper
margin-right: 5px; margin-right: 5px;
} }
.openwebrx-demodulator-button.same-mod {
color: #FFC;
}
.openwebrx-square-button img .openwebrx-square-button img
{ {
height: 27px; height: 27px;
@ -723,8 +735,7 @@ img.openwebrx-mirror-img
color: White; color: White;
} }
#openwebrx-secondary-demod-listbox .openwebrx-secondary-demod-listbox {
{
width: 173px; width: 173px;
height: 27px; height: 27px;
padding-left:3px; padding-left:3px;
@ -923,37 +934,23 @@ img.openwebrx-mirror-img
display: inline-block; display: inline-block;
} }
#openwebrx-panel-wsjt-message, .openwebrx-message-panel {
#openwebrx-panel-packet-message,
#openwebrx-panel-pocsag-message
{
height: 180px; height: 180px;
} }
#openwebrx-panel-wsjt-message tbody, .openwebrx-message-panel tbody {
#openwebrx-panel-packet-message tbody,
#openwebrx-panel-pocsag-message tbody
{
display: block; display: block;
overflow: auto; overflow: auto;
height: 150px; height: 150px;
width: 100%; width: 100%;
} }
#openwebrx-panel-wsjt-message thead tr, .openwebrx-message-panel thead tr {
#openwebrx-panel-packet-message thead tr,
#openwebrx-panel-pocsag-message thead tr
{
display: block; display: block;
} }
#openwebrx-panel-wsjt-message th, .openwebrx-message-panel th,
#openwebrx-panel-wsjt-message td, .openwebrx-message-panel td {
#openwebrx-panel-packet-message th,
#openwebrx-panel-packet-message td,
#openwebrx-panel-pocsag-message th,
#openwebrx-panel-pocsag-message td
{
width: 50px; width: 50px;
text-align: left; text-align: left;
padding: 1px 3px; padding: 1px 3px;
@ -972,6 +969,31 @@ img.openwebrx-mirror-img
width: 70px; width: 70px;
} }
#openwebrx-panel-js8-message .message {
width: 465px;
max-width: 465px;
}
#openwebrx-panel-js8-message td.message {
white-space: nowrap;
overflow: hidden;
display: flex;
flex-direction: row-reverse;
}
#openwebrx-panel-js8-message .message div {
flex: 1;
}
#openwebrx-panel-js8-message .decimal {
text-align: right;
width: 35px;
}
#openwebrx-panel-js8-message .decimal.freq {
width: 70px;
}
#openwebrx-panel-packet-message .message { #openwebrx-panel-packet-message .message {
width: 410px; width: 410px;
max-width: 410px; max-width: 410px;
@ -1078,13 +1100,15 @@ img.openwebrx-mirror-img
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-content-container, #openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-content-container,
#openwebrx-panel-digimodes[data-mode="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,
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-select-channel, #openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-select-channel #openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-select-channel,
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-select-channel
{ {
display: none; display: none;
} }
@ -1095,7 +1119,8 @@ img.openwebrx-mirror-img
#openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="jt9"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="ft4"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-canvas-container, #openwebrx-panel-digimodes[data-mode="packet"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-canvas-container #openwebrx-panel-digimodes[data-mode="pocsag"] #openwebrx-digimode-canvas-container,
#openwebrx-panel-digimodes[data-mode="js8"] #openwebrx-digimode-canvas-container
{ {
height: 200px; height: 200px;
margin: -10px; margin: -10px;

View File

@ -1,10 +1,12 @@
<HTML><HEAD> <HTML><HEAD>
<TITLE>OpenWebRX Feature report</TITLE> <TITLE>OpenWebRX Feature report</TITLE>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" /> <link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <link rel="stylesheet" href="static/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="static/css/admin.css" />
<link rel="stylesheet" href="static/css/features.css"> <link rel="stylesheet" href="static/css/features.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.0/showdown.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.0/showdown.min.js"></script>
<script src="static/lib/jquery-3.2.1.min.js"></script> <script src="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/Header.js"></script>
<script src="static/features.js"></script> <script src="static/features.js"></script>
</HEAD><BODY> </HEAD><BODY>
${header} ${header}

View File

@ -3,9 +3,10 @@
<head> <head>
<title>OpenWebRX Settings</title> <title>OpenWebRX Settings</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" /> <link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <link rel="stylesheet" href="static/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="static/css/admin.css" /> <link rel="stylesheet" type="text/css" href="static/css/admin.css" />
<script src="static/lib/jquery-3.2.1.min.js"></script> <script src="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/Header.js"></script>
<script src="https://unpkg.com/location-picker/dist/location-picker.min.js"></script> <script src="https://unpkg.com/location-picker/dist/location-picker.min.js"></script>
<script src="static/settings.js"></script> <script src="static/settings.js"></script>
<meta charset="utf-8"> <meta charset="utf-8">
@ -14,7 +15,7 @@
${header} ${header}
<div class="container"> <div class="container">
<div class="col-12"> <div class="col-12">
<h1>Settings</h1> <h1>General settings</h1>
</div> </div>
${sections} ${sections}
</div> </div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -1,24 +1,23 @@
<div id="webrx-top-container"> <div id="webrx-top-container">
<div id="webrx-top-photo-clip"> <div id="webrx-top-photo-clip">
<img src="static/gfx/openwebrx-top-photo.jpg" id="webrx-top-photo"/> <img src="static/gfx/openwebrx-top-photo.jpg" id="webrx-top-photo" alt="Receiver panorama"/>
<div id="webrx-top-bar" class="webrx-top-bar-parts"> <div id="webrx-top-bar" class="webrx-top-bar-parts">
<a href="https://www.openwebrx.de/" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" /></a> <a href="https://www.openwebrx.de/" target="_blank"><img src="static/gfx/openwebrx-top-logo.png" id="webrx-top-logo" alt="OpenWebRX Logo"/></a>
<a href="http://ha5kfu.sch.bme.hu/" target="_blank"><img src="static/gfx/openwebrx-ha5kfu-top-logo.png" id="webrx-ha5kfu-top-logo" /></a> <img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png" alt="Receiver avatar"/>
<img id="webrx-rx-avatar" class="openwebrx-photo-trigger" src="static/gfx/openwebrx-avatar.png"/>
<div id="webrx-rx-texts"> <div id="webrx-rx-texts">
<div id="webrx-rx-title" class="openwebrx-photo-trigger"></div> <div id="webrx-rx-title" class="openwebrx-photo-trigger"></div>
<div id="webrx-rx-desc" class="openwebrx-photo-trigger"></div> <div id="webrx-rx-desc" class="openwebrx-photo-trigger"></div>
</div> </div>
<div id="openwebrx-rx-details-arrow"> <div id="openwebrx-rx-details-arrow">
<a id="openwebrx-rx-details-arrow-up" class="openwebrx-photo-trigger"><img src="static/gfx/openwebrx-rx-details-arrow-up.png" /></a> <a id="openwebrx-rx-details-arrow-up" class="openwebrx-photo-trigger" style="display: none;"><img src="static/gfx/openwebrx-rx-details-arrow-up.png" /></a>
<a id="openwebrx-rx-details-arrow-down" class="openwebrx-photo-trigger"><img src="static/gfx/openwebrx-rx-details-arrow.png" /></a> <a id="openwebrx-rx-details-arrow-down" class="openwebrx-photo-trigger"><img src="static/gfx/openwebrx-rx-details-arrow.png" /></a>
</div> </div>
<section id="openwebrx-main-buttons"> <section id="openwebrx-main-buttons">
<div class="button" data-toggle-panel="openwebrx-panel-status"><img src="static/gfx/openwebrx-panel-status.png" /><br/>Status</div> <div class="button" data-toggle-panel="openwebrx-panel-status"><img src="static/gfx/openwebrx-panel-status.png" alt="Status"/><br/>Status</div>
<div class="button" data-toggle-panel="openwebrx-panel-log"><img src="static/gfx/openwebrx-panel-log.png" /><br/>Log</div> <div class="button" data-toggle-panel="openwebrx-panel-log"><img src="static/gfx/openwebrx-panel-log.png" alt="Log"/><br/>Log</div>
<div class="button" data-toggle-panel="openwebrx-panel-receiver"><img src="static/gfx/openwebrx-panel-receiver.png" /><br/>Receiver</div> <div class="button" data-toggle-panel="openwebrx-panel-receiver"><img src="static/gfx/openwebrx-panel-receiver.png" alt="Receiver"/><br/>Receiver</div>
<a class="button" href="map" target="_blank"><img src="static/gfx/openwebrx-panel-map.png" /><br/>Map</a> <a class="button" href="map" target="openwebrx-map"><img src="static/gfx/openwebrx-panel-map.png" alt="Map"/><br/>Map</a>
<a class="button" href="admin" target="_blank"><img src="static/gfx/openwebrx-panel-settings.png" /><br/>Settings</a> ${settingslink}
</section> </section>
</div> </div>
<div id="webrx-rx-photo-title"></div> <div id="webrx-rx-photo-title"></div>

View File

@ -24,14 +24,7 @@
<head> <head>
<title>OpenWebRX | Open Source SDR Web App for Everyone!</title> <title>OpenWebRX | Open Source SDR Web App for Everyone!</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" /> <link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<script src="static/openwebrx.js"></script> <script src="compiled/receiver.js"></script>
<script src="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/jquery.nanoscroller.js"></script>
<script src="static/lib/BookmarkBar.js"></script>
<script src="static/lib/AudioEngine.js"></script>
<script src="static/lib/ProgressBar.js"></script>
<script src="static/lib/Measurement.js"></script>
<script src="static/lib/FrequencyDisplay.js"></script>
<link rel="stylesheet" type="text/css" href="static/lib/nanoscroller.css" /> <link rel="stylesheet" type="text/css" href="static/lib/nanoscroller.css" />
<link rel="stylesheet" type="text/css" href="static/css/openwebrx.css" /> <link rel="stylesheet" type="text/css" href="static/css/openwebrx.css" />
<meta charset="utf-8"> <meta charset="utf-8">
@ -67,7 +60,7 @@
</div> </div>
</div> </div>
</div> </div>
<table class="openwebrx-panel" id="openwebrx-panel-wsjt-message" style="display: none; width: 619px;" data-panel-name="wsjt-message"> <table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-wsjt-message" style="display: none; width: 619px;" data-panel-name="wsjt-message">
<thead><tr> <thead><tr>
<th>UTC</th> <th>UTC</th>
<th class="decimal">dB</th> <th class="decimal">dB</th>
@ -77,7 +70,15 @@
</tr></thead> </tr></thead>
<tbody></tbody> <tbody></tbody>
</table> </table>
<table class="openwebrx-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message"> <table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-js8-message" style="display:none; width: 619px;" data-panel-name="js8-message">
<thead><tr>
<th>UTC</th>
<th class="decimal freq">Freq</th>
<th class="message">Message</th>
</tr></thead>
<tbody></tbody>
</table>
<table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-packet-message" style="display: none; width: 619px;" data-panel-name="aprs-message">
<thead><tr> <thead><tr>
<th>UTC</th> <th>UTC</th>
<th class="callsign">Callsign</th> <th class="callsign">Callsign</th>
@ -86,7 +87,7 @@
</tr></thead> </tr></thead>
<tbody></tbody> <tbody></tbody>
</table> </table>
<table class="openwebrx-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message"> <table class="openwebrx-panel openwebrx-message-panel" id="openwebrx-panel-pocsag-message" style="display: none; width: 619px;" data-panel-name="pocsag-message">
<thead><tr> <thead><tr>
<th class="address">Address</th> <th class="address">Address</th>
<th class="message">Message</th> <th class="message">Message</th>
@ -126,27 +127,30 @@
<div class="openwebrx-panel-inner nano" id="openwebrx-log-scroll"> <div class="openwebrx-panel-inner nano" id="openwebrx-log-scroll">
<div class="nano-content"> <div class="nano-content">
<div id="openwebrx-client-log-title">OpenWebRX client log</div> <div id="openwebrx-client-log-title">OpenWebRX client log</div>
<div>Author contact: <a href="http://www.justjakob.de/" target="_blank">Jakob Ketterl, DD5JFK</a></div> <div>
Author contact: <a href="http://www.justjakob.de/" target="_blank">Jakob Ketterl, DD5JFK</a> |
<a href="https://www.openwebrx.de" target="_blank">OpenWebRX homepage</a>
</div>
<div>Support and information: <a href="https://groups.io/g/openwebrx" target="_blank">Groups.io Mailinglist</a></div> <div>Support and information: <a href="https://groups.io/g/openwebrx" target="_blank">Groups.io Mailinglist</a></div>
<div id="openwebrx-debugdiv"></div> <div id="openwebrx-debugdiv"></div>
</div> </div>
</div> </div>
</div> </div>
<div class="openwebrx-panel" id="openwebrx-panel-status" data-panel-name="status" style="width: 615px;" data-panel-transparent="true"> <div class="openwebrx-panel" id="openwebrx-panel-status" data-panel-name="status" style="width: 615px;" data-panel-transparent="true">
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-buffer"> <span class="openwebrx-progressbar-text">Audio buffer [0 ms]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-audio-buffer" data-type="audiobuffer"></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-output"> <span class="openwebrx-progressbar-text">Audio output [0 sps]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-audio-output" data-type="audiooutput"></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-audio-speed"> <span class="openwebrx-progressbar-text">Audio stream [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-audio-speed" data-type="audiospeed"></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-network-speed"> <span class="openwebrx-progressbar-text">Network usage [0 kbps]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-network-speed" data-type="networkspeed"></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-server-cpu"> <span class="openwebrx-progressbar-text">Server CPU [0%]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-server-cpu" data-type="cpu"></div>
<div class="openwebrx-progressbar" id="openwebrx-bar-clients"> <span class="openwebrx-progressbar-text">Clients [1]</span><div class="openwebrx-progressbar-bar"></div></div> <div class="openwebrx-progressbar" id="openwebrx-bar-clients" data-type="clients"></div>
</div> </div>
</div> </div>
<div id="openwebrx-panels-container-right"> <div id="openwebrx-panels-container-right">
<div class="openwebrx-panel" id="openwebrx-panel-receiver" data-panel-name="client-params" style="width: 259px;"> <div class="openwebrx-panel" id="openwebrx-panel-receiver" data-panel-name="client-params" style="width: 259px;">
<div class="openwebrx-panel-line frequencies-container"> <div class="openwebrx-panel-line frequencies-container">
<div class="frequencies"> <div class="frequencies">
<div id="webrx-actual-freq"></div> <div class="webrx-actual-freq"></div>
<div id="webrx-mouse-freq"></div> <div class="webrx-mouse-freq"></div>
</div> </div>
<div class="openwebrx-button openwebrx-square-button openwebrx-bookmark-button" style="display:none;" title="Add bookmark..."> <div class="openwebrx-button openwebrx-square-button openwebrx-bookmark-button" style="display:none;" title="Add bookmark...">
<img src="static/gfx/openwebrx-bookmark.png"> <img src="static/gfx/openwebrx-bookmark.png">
@ -156,47 +160,7 @@
<select id="openwebrx-sdr-profiles-listbox" onchange="sdr_profile_changed();"> <select id="openwebrx-sdr-profiles-listbox" onchange="sdr_profile_changed();">
</select> </select>
</div> </div>
<div class="openwebrx-panel-line openwebrx-panel-flex-line"> <div class="openwebrx-modes openwebrx-panel-line"></div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nfm"
onclick="demodulator_analog_replace('nfm');">FM</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-am"
onclick="demodulator_analog_replace('am');">AM</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-lsb"
onclick="demodulator_analog_replace('lsb');">LSB</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-usb"
onclick="demodulator_analog_replace('usb');">USB</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-cw"
onclick="demodulator_analog_replace('cw');">CW</div>
</div>
<div class="openwebrx-panel-line openwebrx-panel-flex-line">
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dmr"
style="display:none;" data-feature="digital_voice_digiham"
onclick="demodulator_analog_replace('dmr');">DMR</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dstar"
style="display:none;" data-feature="digital_voice_dsd"
onclick="demodulator_analog_replace('dstar');">DStar</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-nxdn"
style="display:none;" data-feature="digital_voice_dsd"
onclick="demodulator_analog_replace('nxdn');">NXDN</div>
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-ysf"
style="display:none;" data-feature="digital_voice_digiham"
onclick="demodulator_analog_replace('ysf');">YSF</div>
</div>
<div class="openwebrx-panel-line openwebrx-panel-flex-line">
<div class="openwebrx-button openwebrx-demodulator-button" id="openwebrx-button-dig" onclick="demodulator_digital_replace_last();">DIG</div>
<select id="openwebrx-secondary-demod-listbox" onchange="secondary_demod_listbox_changed();">
<option value="none"></option>
<option value="bpsk31">BPSK31</option>
<option value="bpsk63">BPSK63</option>
<option value="ft8" data-feature="wsjt-x">FT8</option>
<option value="wspr" data-feature="wsjt-x">WSPR</option>
<option value="jt65" data-feature="wsjt-x">JT65</option>
<option value="jt9" data-feature="wsjt-x">JT9</option>
<option value="ft4" data-feature="wsjt-x">FT4</option>
<option value="packet" data-feature="packet">Packet</option>
<option value="pocsag" data-feature="pocsag">Pocsag</option>
</select>
</div>
<div class="openwebrx-panel-line"> <div class="openwebrx-panel-line">
<div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><img src="static/gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div> <div title="Mute on/off" id="openwebrx-mute-off" class="openwebrx-button" onclick="toggleMute();"><img src="static/gfx/openwebrx-speaker.png" class="openwebrx-sliderbtn-img" id="openwebrx-mute-img"></div>
<input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()"> <input title="Volume" id="openwebrx-panel-volume" class="openwebrx-panel-slider" type="range" min="0" max="150" value="50" step="1" onchange="updateVolume()" oninput="updateVolume()">
@ -204,8 +168,8 @@
<input title="Waterfall minimum level" id="openwebrx-waterfall-color-min" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(0);" oninput="updateVolume()"> <input title="Waterfall minimum level" id="openwebrx-waterfall-color-min" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(0);" oninput="updateVolume()">
</div> </div>
<div class="openwebrx-panel-line"> <div class="openwebrx-panel-line">
<div title="Auto-set squelch level" id="openwebrx-squelch-default" class="openwebrx-button" onclick="setSquelchToAuto()"><img src="static/gfx/openwebrx-squelch-button.png" class="openwebrx-sliderbtn-img"></div> <div title="Auto-set squelch level" class="openwebrx-squelch-default openwebrx-button"><img src="static/gfx/openwebrx-squelch-button.png" class="openwebrx-sliderbtn-img"></div>
<input title="Squelch" id="openwebrx-panel-squelch" class="openwebrx-panel-slider" type="range" min="-150" max="0" value="-150" step="1" onchange="updateSquelch()" oninput="updateSquelch()"> <input title="Squelch" class="openwebrx-squelch-slider openwebrx-panel-slider" type="range" min="-150" max="0" value="-150" step="1">
<div title="Set waterfall colors to default" id="openwebrx-waterfall-colors-default" class="openwebrx-button" onclick="waterfallColorsDefault()"><img src="static/gfx/openwebrx-waterfall-default.png" class="openwebrx-sliderbtn-img"></div> <div title="Set waterfall colors to default" id="openwebrx-waterfall-colors-default" class="openwebrx-button" onclick="waterfallColorsDefault()"><img src="static/gfx/openwebrx-waterfall-default.png" class="openwebrx-sliderbtn-img"></div>
<input title="Waterfall maximum level" id="openwebrx-waterfall-color-max" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(1);" oninput="updateVolume()"> <input title="Waterfall maximum level" id="openwebrx-waterfall-color-max" class="openwebrx-panel-slider" type="range" min="-200" max="100" value="50" step="1" onchange="updateWaterfallColors(1);" oninput="updateVolume()">
</div> </div>
@ -250,17 +214,7 @@
</div> </div>
<div class="form-field"> <div class="form-field">
<label for="modulation">Modulation:</label> <label for="modulation">Modulation:</label>
<select name="modulation" id="modulation"> <select name="modulation" id="modulation"></select>
<option value="nfm">FM</option>
<option value="am">AM</option>
<option value="usb">USB</option>
<option value="lsb">LSB</option>
<option value="cw">CW</option>
<option value="dmr">DMR</option>
<option value="dstar">D-Star</option>
<option value="nxdn">NXDN</option>
<option value="ysf">YSF</option>
</select>
</div> </div>
<div class="buttons"> <div class="buttons">
<div class="openwebrx-button" data-action="cancel">Cancel</div> <div class="openwebrx-button" data-action="cancel">Cancel</div>

View File

@ -8,12 +8,10 @@ function BookmarkBar() {
var $bookmark = $(e.target).closest('.bookmark'); var $bookmark = $(e.target).closest('.bookmark');
me.$container.find('.bookmark').removeClass('selected'); me.$container.find('.bookmark').removeClass('selected');
var b = $bookmark.data(); var b = $bookmark.data();
if (!b || !b.frequency || (!b.modulation && !b.digital_modulation)) return; if (!b || !b.frequency || !b.modulation) return;
demodulators[0].set_offset_frequency(b.frequency - center_freq); me.getDemodulator().set_offset_frequency(b.frequency - center_freq);
if (b.modulation) { if (b.modulation) {
demodulator_analog_replace(b.modulation); me.getDemodulatorPanel().setMode(b.modulation);
} else if (b.digital_modulation) {
demodulator_digital_replace(b.digital_modulation);
} }
$bookmark.addClass('selected'); $bookmark.addClass('selected');
}); });
@ -104,40 +102,26 @@ BookmarkBar.prototype.render = function(){
}; };
BookmarkBar.prototype.showEditDialog = function(bookmark) { BookmarkBar.prototype.showEditDialog = function(bookmark) {
var $form = this.$dialog.find("form");
if (!bookmark) { if (!bookmark) {
bookmark = { bookmark = {
name: "", name: "",
frequency: center_freq + demodulators[0].offset_frequency, frequency: center_freq + this.getDemodulator().get_offset_frequency(),
modulation: demodulators[0].subtype modulation: this.getDemodulator().get_secondary_demod() || this.getDemodulator().get_modulation()
} }
} }
['name', 'frequency', 'modulation'].forEach(function(key){ this.$dialog.bookmarkDialog().setValues(bookmark);
$form.find('#' + key).val(bookmark[key]);
});
this.$dialog.data('id', bookmark.id);
this.$dialog.show(); this.$dialog.show();
this.$dialog.find('#name').focus(); this.$dialog.find('#name').focus();
}; };
BookmarkBar.prototype.storeBookmark = function() { BookmarkBar.prototype.storeBookmark = function() {
var me = this; var me = this;
var bookmark = {}; var bookmark = this.$dialog.bookmarkDialog().getValues();
var valid = true; if (!bookmark) return;
['name', 'frequency', 'modulation'].forEach(function(key){
var $input = me.$dialog.find('#' + key);
valid = valid && $input[0].checkValidity();
bookmark[key] = $input.val();
});
if (!valid) {
me.$dialog.find("form :submit").click();
return;
}
bookmark.frequency = Number(bookmark.frequency); bookmark.frequency = Number(bookmark.frequency);
var bookmarks = me.localBookmarks.getBookmarks(); var bookmarks = me.localBookmarks.getBookmarks();
bookmark.id = me.$dialog.data('id');
if (!bookmark.id) { if (!bookmark.id) {
if (bookmarks.length) { if (bookmarks.length) {
bookmark.id = 1 + Math.max.apply(Math, bookmarks.map(function(b){ return b.id || 0; })); bookmark.id = 1 + Math.max.apply(Math, bookmarks.map(function(b){ return b.id || 0; }));
@ -154,6 +138,14 @@ BookmarkBar.prototype.storeBookmark = function() {
me.$dialog.hide(); me.$dialog.hide();
}; };
BookmarkBar.prototype.getDemodulatorPanel = function() {
return $('#openwebrx-panel-receiver').demodulatorPanel();
};
BookmarkBar.prototype.getDemodulator = function() {
return this.getDemodulatorPanel().getDemodulator();
};
BookmarkLocalStorage = function(){ BookmarkLocalStorage = function(){
}; };
@ -171,7 +163,3 @@ BookmarkLocalStorage.prototype.deleteBookmark = function(data) {
bookmarks = bookmarks.filter(function(b) { return b.id !== data; }); bookmarks = bookmarks.filter(function(b) { return b.id !== data; });
this.setBookmarks(bookmarks); this.setBookmarks(bookmarks);
}; };

View File

@ -0,0 +1,36 @@
$.fn.bookmarkDialog = function() {
var $el = this;
return {
setModes: function(modes) {
$el.find('#modulation').html(modes.filter(function(m){
return m.isAvailable();
}).map(function(m) {
return '<option value="' + m.modulation + '">' + m.name + '</option>';
}).join(''));
return this;
},
setValues: function(bookmark) {
var $form = $el.find('form');
['name', 'frequency', 'modulation'].forEach(function(key){
$form.find('#' + key).val(bookmark[key]);
});
$el.data('id', bookmark.id || false);
return this;
},
getValues: function() {
var bookmark = {};
var valid = true;
['name', 'frequency', 'modulation'].forEach(function(key){
var $input = $el.find('#' + key);
valid = valid && $input[0].checkValidity();
bookmark[key] = $input.val();
});
if (!valid) {
$el.find("form :submit").click();
return;
}
bookmark.id = $el.data('id');
return bookmark;
}
}
}

356
htdocs/lib/Demodulator.js Normal file
View File

@ -0,0 +1,356 @@
function Filter(demodulator) {
this.demodulator = demodulator;
this.min_passband = 100;
}
Filter.prototype.getLimits = function() {
var max_bw;
if (this.demodulator.get_secondary_demod() === 'pocsag') {
max_bw = 12500;
} else {
max_bw = (audioEngine.getOutputRate() / 2) - 1;
}
return {
high: max_bw,
low: -max_bw
};
};
function Envelope(demodulator) {
this.demodulator = demodulator;
this.dragged_range = Demodulator.draggable_ranges.none;
}
Envelope.prototype.draw = function(visible_range){
this.visible_range = visible_range;
var line = center_freq + this.demodulator.offset_frequency;
// ____
// Draws a standard filter envelope like this: _/ \_
// Parameters are given in offset frequency (Hz).
// Envelope is drawn on the scale canvas.
// A "drag range" object is returned, containing information about the draggable areas of the envelope
// (beginning, ending and the line showing the offset frequency).
var env_bounding_line_w = 5; //
var env_att_w = 5; // _______ ___env_h2 in px ___|_____
var env_h1 = 17; // _/| \_ ___env_h1 in px _/ |_ \_
var env_h2 = 5; // |||env_att_line_w |_env_lineplus
var env_lineplus = 1; // ||env_bounding_line_w
var env_line_click_area = 6;
//range=get_visible_freq_range();
var from = center_freq + this.demodulator.offset_frequency + this.demodulator.low_cut;
var from_px = scale_px_from_freq(from, range);
var to = center_freq + this.demodulator.offset_frequency + this.demodulator.high_cut;
var to_px = scale_px_from_freq(to, range);
if (to_px < from_px) /* swap'em */ {
var temp_px = to_px;
to_px = from_px;
from_px = temp_px;
}
from_px -= (env_att_w + env_bounding_line_w);
to_px += (env_att_w + env_bounding_line_w);
// do drawing:
scale_ctx.lineWidth = 3;
var color = this.color || '#ffff00'; // yellow
scale_ctx.strokeStyle = color;
scale_ctx.fillStyle = color;
var drag_ranges = {envelope_on_screen: false, line_on_screen: false};
if (!(to_px < 0 || from_px > window.innerWidth)) // out of screen?
{
drag_ranges.beginning = {x1: from_px, x2: from_px + env_bounding_line_w + env_att_w};
drag_ranges.ending = {x1: to_px - env_bounding_line_w - env_att_w, x2: to_px};
drag_ranges.whole_envelope = {x1: from_px, x2: to_px};
drag_ranges.envelope_on_screen = true;
scale_ctx.beginPath();
scale_ctx.moveTo(from_px, env_h1);
scale_ctx.lineTo(from_px + env_bounding_line_w, env_h1);
scale_ctx.lineTo(from_px + env_bounding_line_w + env_att_w, env_h2);
scale_ctx.lineTo(to_px - env_bounding_line_w - env_att_w, env_h2);
scale_ctx.lineTo(to_px - env_bounding_line_w, env_h1);
scale_ctx.lineTo(to_px, env_h1);
scale_ctx.globalAlpha = 0.3;
scale_ctx.fill();
scale_ctx.globalAlpha = 1;
scale_ctx.stroke();
}
if (typeof line !== "undefined") // out of screen?
{
var line_px = scale_px_from_freq(line, range);
if (!(line_px < 0 || line_px > window.innerWidth)) {
drag_ranges.line = {x1: line_px - env_line_click_area / 2, x2: line_px + env_line_click_area / 2};
drag_ranges.line_on_screen = true;
scale_ctx.moveTo(line_px, env_h1 + env_lineplus);
scale_ctx.lineTo(line_px, env_h2 - env_lineplus);
scale_ctx.stroke();
}
}
this.drag_ranges = drag_ranges;
};
Envelope.prototype.drag_start = function(x, key_modifiers){
this.key_modifiers = key_modifiers;
this.dragged_range = this.where_clicked(x, this.drag_ranges, key_modifiers);
this.drag_origin = {
x: x,
low_cut: this.demodulator.low_cut,
high_cut: this.demodulator.high_cut,
offset_frequency: this.demodulator.offset_frequency
};
return this.dragged_range !== Demodulator.draggable_ranges.none;
};
Envelope.prototype.where_clicked = function(x, drag_ranges, key_modifiers) { // Check exactly what the user has clicked based on ranges returned by envelope_draw().
var in_range = function (x, range) {
return range.x1 <= x && range.x2 >= x;
};
var dr = Demodulator.draggable_ranges;
if (key_modifiers.shiftKey) {
//Check first: shift + center drag emulates BFO knob
if (drag_ranges.line_on_screen && in_range(x, drag_ranges.line)) return dr.bfo;
//Check second: shift + envelope drag emulates PBF knob
if (drag_ranges.envelope_on_screen && in_range(x, drag_ranges.whole_envelope)) return dr.pbs;
}
if (drag_ranges.envelope_on_screen) {
// For low and high cut:
if (in_range(x, drag_ranges.beginning)) return dr.beginning;
if (in_range(x, drag_ranges.ending)) return dr.ending;
// Last priority: having clicked anything else on the envelope, without holding the shift key
if (in_range(x, drag_ranges.whole_envelope)) return dr.anything_else;
}
return dr.none; //User doesn't drag the envelope for this demodulator
};
Envelope.prototype.drag_move = function(x) {
var dr = Demodulator.draggable_ranges;
var new_value;
if (this.dragged_range === dr.none) return false; // we return if user is not dragging (us) at all
var freq_change = Math.round(this.visible_range.hps * (x - this.drag_origin.x));
//dragging the line in the middle of the filter envelope while holding Shift does emulate
//the BFO knob on radio equipment: moving offset frequency, while passband remains unchanged
//Filter passband moves in the opposite direction than dragged, hence the minus below.
var minus = (this.dragged_range === dr.bfo) ? -1 : 1;
//dragging any other parts of the filter envelope while holding Shift does emulate the PBS knob
//(PassBand Shift) on radio equipment: PBS does move the whole passband without moving the offset
//frequency.
if (this.dragged_range === dr.beginning || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) {
//we don't let low_cut go beyond its limits
if ((new_value = this.drag_origin.low_cut + minus * freq_change) < this.demodulator.filter.getLimits().low) return true;
//nor the filter passband be too small
if (this.demodulator.high_cut - new_value < this.demodulator.filter.min_passband) return true;
//sanity check to prevent GNU Radio "firdes check failed: fa <= fb"
if (new_value >= this.demodulator.high_cut) return true;
this.demodulator.setLowCut(new_value);
}
if (this.dragged_range === dr.ending || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) {
//we don't let high_cut go beyond its limits
if ((new_value = this.drag_origin.high_cut + minus * freq_change) > this.demodulator.filter.getLimits().high) return true;
//nor the filter passband be too small
if (new_value - this.demodulator.low_cut < this.demodulator.filter.min_passband) return true;
//sanity check to prevent GNU Radio "firdes check failed: fa <= fb"
if (new_value <= this.demodulator.low_cut) return true;
this.demodulator.setHighCut(new_value);
}
if (this.dragged_range === dr.anything_else || this.dragged_range === dr.bfo) {
//when any other part of the envelope is dragged, the offset frequency is changed (whole passband also moves with it)
new_value = this.drag_origin.offset_frequency + freq_change;
if (new_value > bandwidth / 2 || new_value < -bandwidth / 2) return true; //we don't allow tuning above Nyquist frequency :-)
this.demodulator.set_offset_frequency(new_value);
}
//now do the actual modifications:
//mkenvelopes(this.visible_range);
//this.demodulator.set();
return true;
};
Envelope.prototype.drag_end = function(){
var to_return = this.dragged_range !== Demodulator.draggable_ranges.none; //this part is required for cliking anywhere on the scale to set offset
this.dragged_range = Demodulator.draggable_ranges.none;
return to_return;
};
//******* class Demodulator_default_analog *******
// This can be used as a base for basic audio demodulators.
// It already supports most basic modulations used for ham radio and commercial services: AM/FM/LSB/USB
function Demodulator(offset_frequency, modulation) {
this.offset_frequency = offset_frequency;
this.envelope = new Envelope(this);
this.color = Demodulator.get_next_color();
this.modulation = modulation;
this.filter = new Filter(this);
this.squelch_level = -150;
this.dmr_filter = 3;
this.started = false;
this.state = {};
this.secondary_demod = false;
var mode = Modes.findByModulation(modulation);
if (mode) {
this.low_cut = mode.bandpass.low_cut;
this.high_cut = mode.bandpass.high_cut;
}
this.listeners = {
"frequencychange": [],
"squelchchange": []
};
}
//ranges on filter envelope that can be dragged:
Demodulator.draggable_ranges = {
none: 0,
beginning: 1 /*from*/,
ending: 2 /*to*/,
anything_else: 3,
bfo: 4 /*line (while holding shift)*/,
pbs: 5
}; //to which parameter these correspond in envelope_draw()
Demodulator.color_index = 0;
Demodulator.colors = ["#ffff00", "#00ff00", "#00ffff", "#058cff", "#ff9600", "#a1ff39", "#ff4e39", "#ff5dbd"];
Demodulator.get_next_color = function() {
if (this.color_index >= this.colors.length) this.color_index = 0;
return (this.colors[this.color_index++]);
}
Demodulator.prototype.on = function(event, handler) {
this.listeners[event].push(handler);
};
Demodulator.prototype.emit = function(event, params) {
this.listeners[event].forEach(function(fn) {
fn(params);
});
};
Demodulator.prototype.set_offset_frequency = function(to_what) {
if (to_what > bandwidth / 2 || to_what < -bandwidth / 2) return;
to_what = Math.round(to_what);
if (this.offset_frequency === to_what) {
return;
}
this.offset_frequency = to_what;
this.set();
this.emit("frequencychange", to_what);
mkenvelopes(get_visible_freq_range());
};
Demodulator.prototype.get_offset_frequency = function() {
return this.offset_frequency;
};
Demodulator.prototype.get_modulation = function() {
return this.modulation;
};
Demodulator.prototype.start = function() {
this.started = true;
this.set();
ws.send(JSON.stringify({
"type": "dspcontrol",
"action": "start"
}));
};
// TODO check if this is actually used
Demodulator.prototype.stop = function() {
};
Demodulator.prototype.send = function(params) {
ws.send(JSON.stringify({"type": "dspcontrol", "params": params}));
}
Demodulator.prototype.set = function () { //this function sends demodulator parameters to the server
if (!this.started) return;
var params = {
"low_cut": this.low_cut,
"high_cut": this.high_cut,
"offset_freq": this.offset_frequency,
"mod": this.modulation,
"dmr_filter": this.dmr_filter,
"squelch_level": this.squelch_level,
"secondary_mod": this.secondary_demod,
"secondary_offset_freq": this.secondary_offset_freq
};
var to_send = {};
for (var key in params) {
if (!(key in this.state) || params[key] !== this.state[key]) {
to_send[key] = params[key];
}
}
if (Object.keys(to_send).length > 0) {
this.send(to_send);
for (var key in to_send) {
this.state[key] = to_send[key];
}
}
mkenvelopes(get_visible_freq_range());
};
Demodulator.prototype.setSquelch = function(squelch) {
if (this.squelch_level == squelch) {
return;
}
this.squelch_level = squelch;
this.set();
this.emit("squelchchange", squelch);
};
Demodulator.prototype.getSquelch = function() {
return this.squelch_level;
};
Demodulator.prototype.setDmrFilter = function(dmr_filter) {
this.dmr_filter = dmr_filter;
this.set();
};
Demodulator.prototype.setBandpass = function(bandpass) {
this.bandpass = bandpass;
this.low_cut = bandpass.low_cut;
this.high_cut = bandpass.high_cut;
this.set();
};
Demodulator.prototype.setLowCut = function(low_cut) {
this.low_cut = low_cut;
this.set();
};
Demodulator.prototype.setHighCut = function(high_cut) {
this.high_cut = high_cut;
this.set();
};
Demodulator.prototype.getBandpass = function() {
return {
low_cut: this.low_cut,
high_cut: this.high_cut
};
};
Demodulator.prototype.set_secondary_demod = function(secondary_demod) {
if (this.secondary_demod === secondary_demod) {
return;
}
this.secondary_demod = secondary_demod;
this.set();
};
Demodulator.prototype.get_secondary_demod = function() {
return this.secondary_demod;
};
Demodulator.prototype.set_secondary_offset_freq = function(secondary_offset) {
if (this.secondary_offset_freq === secondary_offset) {
return;
}
this.secondary_offset_freq = secondary_offset;
this.set();
};

View File

@ -0,0 +1,333 @@
function DemodulatorPanel(el) {
var self = this;
self.el = el;
self.demodulator = null;
self.mode = null;
var displayEl = el.find('.webrx-actual-freq')
this.tuneableFrequencyDisplay = displayEl.tuneableFrequencyDisplay();
displayEl.on('frequencychange', function(event, freq) {
self.getDemodulator().set_offset_frequency(freq - self.center_freq);
});
Modes.registerModePanel(this);
el.on('click', '.openwebrx-demodulator-button', function() {
var modulation = $(this).data('modulation');
if (modulation) {
self.setMode(modulation);
} else {
self.disableDigiMode();
}
});
el.on('change', '.openwebrx-secondary-demod-listbox', function() {
var value = $(this).val();
if (value === 'none') {
self.disableDigiMode();
} else {
self.setMode(value);
}
});
el.on('click', '.openwebrx-squelch-default', function() {
if (!self.squelchAvailable()) return;
el.find('.openwebrx-squelch-slider').val(getLogSmeterValue(smeter_level) + 10);
self.updateSquelch();
});
el.on('change', '.openwebrx-squelch-slider', function() {
self.updateSquelch();
});
window.addEventListener('hashchange', function() {
self.onHashChange();
});
};
DemodulatorPanel.prototype.render = function() {
var available = Modes.getModes().filter(function(m){ return m.isAvailable(); });
var normalModes = available.filter(function(m){ return m.type === 'analog'; });
var digiModes = available.filter(function(m){ return m.type === 'digimode'; });
var html = []
var buttons = normalModes.map(function(m){
return $(
'<div ' +
'class="openwebrx-button openwebrx-demodulator-button" ' +
'data-modulation="' + m.modulation + '" ' +
'id="openwebrx-button-' + m.modulation + '" r' +
'>' + m.name + '</div>'
);
});
var index = 0;
var arrayLength = buttons.length;
var chunks = [];
for (index = 0; index < arrayLength; index += 5) {
chunks.push(buttons.slice(index, index + 5));
}
html.push.apply(html, chunks.map(function(chunk){
$line = $('<div class="openwebrx-panel-line openwebrx-panel-flex-line"></div>');
$line.append.apply($line, chunk);
return $line
}));
html.push($(
'<div class="openwebrx-panel-line openwebrx-panel-flex-line">' +
'<div class="openwebrx-button openwebrx-demodulator-button openwebrx-button-dig">DIG</div>' +
'<select class="openwebrx-secondary-demod-listbox">' +
'<option value="none"></option>' +
digiModes.map(function(m){
return '<option value="' + m.modulation + '">' + m.name + '</option>';
}).join('') +
'</select>' +
'</div>'
));
this.el.find(".openwebrx-modes").html(html);
};
DemodulatorPanel.prototype.setMode = function(requestedModulation) {
var mode = Modes.findByModulation(requestedModulation);
if (!mode) {
return;
}
if (this.mode === mode) {
return;
}
if (!mode.isAvailable()) {
divlog('Modulation "' + mode.name + '" not supported. Please check requirements', true);
return;
}
if (mode.type === 'digimode') {
modulation = mode.underlying[0];
} else {
if (this.mode && this.mode.type === 'digimode' && this.mode.underlying.indexOf(requestedModulation) >= 0) {
// keep the mode, just switch underlying modulation
mode = this.mode;
modulation = requestedModulation;
} else {
modulation = mode.modulation;
}
}
var current = this.collectParams();
if (this.demodulator) {
current.offset_frequency = this.demodulator.get_offset_frequency();
current.squelch_level = this.demodulator.getSquelch();
}
this.stopDemodulator();
this.demodulator = new Demodulator(current.offset_frequency, modulation);
this.demodulator.setSquelch(current.squelch_level);
var self = this;
var updateFrequency = function(freq) {
self.tuneableFrequencyDisplay.setFrequency(self.center_freq + freq);
self.updateHash();
};
this.demodulator.on("frequencychange", updateFrequency);
updateFrequency(this.demodulator.get_offset_frequency());
var updateSquelch = function(squelch) {
self.el.find('.openwebrx-squelch-slider').val(squelch);
self.updateHash();
};
this.demodulator.on('squelchchange', updateSquelch);
updateSquelch(this.demodulator.getSquelch());
if (mode.type === 'digimode') {
this.demodulator.set_secondary_demod(mode.modulation);
if (mode.bandpass) {
this.demodulator.setBandpass(mode.bandpass);
}
} else {
this.demodulator.set_secondary_demod(false);
}
this.demodulator.start();
this.mode = mode;
this.updateButtons();
this.updatePanels();
this.updateHash();
};
DemodulatorPanel.prototype.disableDigiMode = function() {
// just a little trick to get out of the digimode
delete this.mode;
this.setMode(this.getDemodulator().get_modulation());
};
DemodulatorPanel.prototype.updatePanels = function() {
var modulation = this.getDemodulator().get_secondary_demod();
$('#openwebrx-panel-digimodes').attr('data-mode', modulation);
toggle_panel("openwebrx-panel-digimodes", !!modulation);
toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4'].indexOf(modulation) >= 0);
toggle_panel("openwebrx-panel-js8-message", modulation == "js8");
toggle_panel("openwebrx-panel-packet-message", modulation === "packet");
toggle_panel("openwebrx-panel-pocsag-message", modulation === "pocsag");
modulation = this.getDemodulator().get_modulation();
var showing = 'openwebrx-panel-metadata-' + modulation;
$(".openwebrx-meta-panel").each(function (_, p) {
toggle_panel(p.id, p.id === showing);
});
clear_metadata();
};
DemodulatorPanel.prototype.getDemodulator = function() {
return this.demodulator;
};
DemodulatorPanel.prototype.collectParams = function() {
var defaults = {
offset_frequency: 0,
squelch_level: -150,
mod: 'nfm'
}
return $.extend(new Object(), defaults, this.initialParams || {}, this.transformHashParams(this.parseHash()));
};
DemodulatorPanel.prototype.startDemodulator = function() {
if (!Modes.initComplete()) return;
var params = this.collectParams();
this._apply(params);
};
DemodulatorPanel.prototype.stopDemodulator = function() {
if (!this.demodulator) {
return;
}
this.demodulator.stop();
this.demodulator = null;
this.mode = null;
}
DemodulatorPanel.prototype._apply = function(params) {
this.setMode(params.mod);
this.getDemodulator().set_offset_frequency(params.offset_frequency);
this.getDemodulator().setSquelch(params.squelch_level);
this.updateButtons();
};
DemodulatorPanel.prototype.setInitialParams = function(params) {
this.initialParams = params;
};
DemodulatorPanel.prototype.onHashChange = function() {
this._apply(this.transformHashParams(this.parseHash()));
};
DemodulatorPanel.prototype.transformHashParams = function(params) {
var ret = {
mod: params.secondary_mod || params.mod
};
if (typeof(params.offset_frequency) !== 'undefined') ret.offset_frequency = params.offset_frequency;
if (typeof(params.sql) !== 'undefined') ret.squelch_level = parseInt(params.sql);
return ret;
};
DemodulatorPanel.prototype.squelchAvailable = function () {
return this.mode && this.mode.squelch;
}
DemodulatorPanel.prototype.updateButtons = function() {
var $buttons = this.el.find(".openwebrx-demodulator-button");
$buttons.removeClass("highlighted").removeClass('same-mod');
var demod = this.getDemodulator()
if (!demod) return;
this.el.find('[data-modulation=' + demod.get_modulation() + ']').addClass("highlighted");
var secondary_demod = demod.get_secondary_demod()
if (secondary_demod) {
this.el.find(".openwebrx-button-dig").addClass("highlighted");
this.el.find('.openwebrx-secondary-demod-listbox').val(secondary_demod);
var mode = Modes.findByModulation(secondary_demod);
if (mode) {
var self = this;
mode.underlying.filter(function(m) {
return m !== demod.get_modulation();
}).forEach(function(m) {
self.el.find('[data-modulation=' + m + ']').addClass('same-mod')
});
}
} else {
this.el.find('.openwebrx-secondary-demod-listbox').val('none');
}
var squelch_disabled = !this.squelchAvailable();
this.el.find('.openwebrx-squelch-slider').prop('disabled', squelch_disabled);
this.el.find('.openwebrx-squelch-default')[squelch_disabled ? 'addClass' : 'removeClass']('disabled');
}
DemodulatorPanel.prototype.setCenterFrequency = function(center_freq) {
if (this.center_freq === center_freq) {
return;
}
this.stopDemodulator();
this.center_freq = center_freq;
this.startDemodulator();
};
DemodulatorPanel.prototype.parseHash = function() {
if (!window.location.hash) {
return {};
}
var params = window.location.hash.substring(1).split(",").map(function(x) {
var harr = x.split('=');
return [harr[0], harr.slice(1).join('=')];
}).reduce(function(params, p){
params[p[0]] = p[1];
return params;
}, {});
return this.validateHash(params);
};
DemodulatorPanel.prototype.validateHash = function(params) {
var self = this;
params = Object.keys(params).filter(function(key) {
if (key == 'freq' || key == 'mod' || key == 'secondary_mod' || key == 'sql') {
return params.freq && Math.abs(params.freq - self.center_freq) < bandwidth;
}
return true;
}).reduce(function(p, key) {
p[key] = params[key];
return p;
}, {});
if (params['freq']) {
params['offset_frequency'] = params['freq'] - self.center_freq;
delete params['freq'];
}
return params;
};
DemodulatorPanel.prototype.updateHash = function() {
var demod = this.getDemodulator();
if (!demod) return;
var self = this;
window.location.hash = $.map({
freq: demod.get_offset_frequency() + self.center_freq,
mod: demod.get_modulation(),
secondary_mod: demod.get_secondary_demod(),
sql: demod.getSquelch(),
}, function(value, key){
if (typeof(value) === 'undefined' || value === false) return undefined;
return key + '=' + value;
}).filter(function(v) {
return !!v;
}).join(',');
};
DemodulatorPanel.prototype.updateSquelch = function() {
var sliderValue = parseInt(this.el.find(".openwebrx-squelch-slider").val());
var demod = this.getDemodulator();
if (demod) demod.setSquelch(sliderValue);
};
$.fn.demodulatorPanel = function(){
if (!this.data('panel')) {
this.data('panel', new DemodulatorPanel(this));
};
return this.data('panel');
};

View File

@ -50,7 +50,6 @@ TuneableFrequencyDisplay.prototype.setupElements = function() {
TuneableFrequencyDisplay.prototype.setupEvents = function() { TuneableFrequencyDisplay.prototype.setupEvents = function() {
var me = this; var me = this;
me.listeners = [];
me.element.on('wheel', function(e){ me.element.on('wheel', function(e){
e.preventDefault(); e.preventDefault();
@ -63,17 +62,13 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() {
if (e.originalEvent.deltaY > 0) delta *= -1; if (e.originalEvent.deltaY > 0) delta *= -1;
var newFrequency = me.frequency + delta; var newFrequency = me.frequency + delta;
me.listeners.forEach(function(l) { me.element.trigger('frequencychange', newFrequency);
l(newFrequency);
});
}); });
var submit = function(){ var submit = function(){
var freq = parseInt(me.input.val()); var freq = parseInt(me.input.val());
if (!isNaN(freq)) { if (!isNaN(freq)) {
me.listeners.forEach(function(l) { me.element.trigger('frequencychange', freq);
l(freq);
});
} }
me.input.hide(); me.input.hide();
me.displayContainer.show(); me.displayContainer.show();
@ -96,6 +91,16 @@ TuneableFrequencyDisplay.prototype.setupEvents = function() {
}); });
}; };
TuneableFrequencyDisplay.prototype.onFrequencyChange = function(listener){ $.fn.frequencyDisplay = function() {
this.listeners.push(listener); if (!this.data('frequencyDisplay')) {
}; this.data('frequencyDisplay', new FrequencyDisplay(this));
}
return this.data('frequencyDisplay');
}
$.fn.tuneableFrequencyDisplay = function() {
if (!this.data('frequencyDisplay')) {
this.data('frequencyDisplay', new TuneableFrequencyDisplay(this));
}
return this.data('frequencyDisplay');
}

77
htdocs/lib/Header.js Normal file
View File

@ -0,0 +1,77 @@
function Header(el) {
this.el = el;
this.el.find('#openwebrx-main-buttons').find('[data-toggle-panel]').click(function () {
toggle_panel($(this).data('toggle-panel'));
});
this.init_rx_photo();
this.download_details();
};
Header.prototype.setDetails = function(details) {
this.el.find('#webrx-rx-title').html(details['receiver_name']);
var query = encodeURIComponent(details['receiver_gps']['lat'] + ',' + details['receiver_gps']['lon']);
this.el.find('#webrx-rx-desc').html(details['receiver_location'] + ' | Loc: ' + details['locator'] + ', ASL: ' + details['receiver_asl'] + ' m, <a href="https://www.google.com/maps/search/?api=1&query=' + query + '" target="_blank">[maps]</a>');
this.el.find('#webrx-rx-photo-title').html(details['photo_title']);
this.el.find('#webrx-rx-photo-desc').html(details['photo_desc']);
};
Header.prototype.init_rx_photo = function() {
this.rx_photo_state = 0;
$.extend($.easing, {
easeOutCubic:function(x) {
return 1 - Math.pow( 1 - x, 3 );
}
});
$('#webrx-top-container').find('.openwebrx-photo-trigger').click(this.toggle_rx_photo.bind(this));
};
Header.prototype.close_rx_photo = function() {
this.rx_photo_state = 0;
this.el.find("#webrx-rx-photo-desc").animate({opacity: 0});
this.el.find("#webrx-rx-photo-title").animate({opacity: 0});
this.el.find('#webrx-top-photo-clip').animate({maxHeight: 67}, {duration: 1000, easing: 'easeOutCubic'});
this.el.find("#openwebrx-rx-details-arrow-down").show();
this.el.find("#openwebrx-rx-details-arrow-up").hide();
}
Header.prototype.open_rx_photo = function() {
this.rx_photo_state = 1;
this.el.find("#webrx-rx-photo-desc").animate({opacity: 1});
this.el.find("#webrx-rx-photo-title").animate({opacity: 1});
this.el.find('#webrx-top-photo-clip').animate({maxHeight: 350}, {duration: 1000, easing: 'easeOutCubic'});
this.el.find("#openwebrx-rx-details-arrow-down").hide();
this.el.find("#openwebrx-rx-details-arrow-up").show();
}
Header.prototype.toggle_rx_photo = function(ev) {
if (ev && ev.target && ev.target.tagName == 'A') {
return;
}
if (this.rx_photo_state) {
this.close_rx_photo();
} else {
this.open_rx_photo();
}
};
Header.prototype.download_details = function() {
var self = this;
$.ajax('api/receiverdetails').done(function(data){
self.setDetails(data);
});
};
$.fn.header = function() {
if (!this.data('header')) {
this.data('header', new Header(this));
}
return this.data('header');
};
$(function(){
$('#webrx-top-container').header();
});

150
htdocs/lib/Js8Threads.js Normal file
View File

@ -0,0 +1,150 @@
Js8Thread = function(el){
this.messages = [];
this.el = el;
};
Js8Thread.prototype.getAverageFrequency = function(){
var total = this.messages.map(function(message){
return message.freq;
}).reduce(function(t, f){
return t + f;
}, 0);
return total / this.messages.length;
};
Js8Thread.prototype.pushMessage = function(message) {
this.messages.push(message);
this.render();
};
Js8Thread.prototype.render = function() {
this.el.html(
'<td>' + this.renderTimestamp(this.getLatestTimestamp()) + '</td>' +
'<td class="decimal freq">' + Math.round(this.getAverageFrequency()) + '</td>' +
'<td class="message"><div>' + this.renderMessages() + '</div></td>'
);
};
Js8Thread.prototype.getLatestTimestamp = function() {
return this.messages[0].timestamp;
};
Js8Thread.prototype.isOpen = function() {
if (!this.messages.length) return true;
var last_message = this.messages[this.messages.length - 1];
return (last_message.thread_type & 2) === 0;
};
Js8Thread.prototype.renderMessages = function() {
var res = [];
for (var i = 0; i < this.messages.length; i++) {
var msg = this.messages[i];
if (msg.thread_type & 1) {
res.push('[ ');
} else if (i === 0 || msg.timestamp - this.messages[i - 1].timestamp > this.getMessageDuration()) {
res.push(' ... ');
}
res.push(msg.msg);
if (msg.thread_type & 2) {
res.push(' ]');
} else if (i === this.messages.length -1) {
res.push(' ... ');
}
}
return res.join('');
};
Js8Thread.prototype.getMessageDuration = function() {
switch (this.getMode()) {
case 'A':
return 15000;
case 'E':
return 30000;
case 'B':
return 10000;
case 'C':
return 6000;
}
};
Js8Thread.prototype.getMode = function() {
// we filter messages by mode, so the first one is as good as any
if (!this.messages.length) return;
return this.messages[0].mode;
};
Js8Thread.prototype.acceptsMode = function(mode) {
var currentMode = this.getMode();
return typeof(currentMode) === 'undefined' || currentMode === mode;
};
Js8Thread.prototype.renderTimestamp = function(timestamp) {
var t = new Date(timestamp);
var pad = function (i) {
return ('' + i).padStart(2, "0");
};
return pad(t.getUTCHours()) + pad(t.getUTCMinutes()) + pad(t.getUTCSeconds());
};
Js8Thread.prototype.purgeOldMessages = function() {
var now = new Date().getTime();
this.messages = this.messages.filter(function(m) {
// keep messages around for 20 minutes
return now - m.timestamp < 20 * 60 * 1000;
});
if (!this.messages.length) {
this.el.remove();
} else {
this.render();
}
return this.messages.length;
};
Js8Threader = function(el){
this.threads = [];
this.tbody = $(el).find('tbody');
var me = this;
this.interval = setInterval(function(){
me.purgeOldMessages();
}, 15000);
};
Js8Threader.prototype.purgeOldMessages = function() {
this.threads = this.threads.filter(function(t) {
return t.purgeOldMessages();
});
};
Js8Threader.prototype.findThread = function(freq, mode) {
var matching = this.threads.filter(function(thread) {
// max frequency deviation: 5 Hz. this may be a little tight.
return thread.isOpen() && thread.acceptsMode(mode) && Math.abs(thread.getAverageFrequency() - freq) <= 5;
});
matching.sort(function(a, b){
return b.getLatestTimestamp() - a.getLatestTimestamp();
});
return matching[0] || false;
};
Js8Threader.prototype.pushMessage = function(message) {
var thread;
// only look for exising threads if the message is not a starting message
if ((message.thread_type & 1) === 0) {
thread = this.findThread(message.freq, message.mode);
}
if (!thread) {
var line = $("<tr></tr>");
this.tbody.append(line);
thread = new Js8Thread(line);
this.threads.push(thread);
}
thread.pushMessage(message);
this.tbody.scrollTop(this.tbody[0].scrollHeight);
};
$.fn.js8 = function() {
if (!this.data('threader')) {
this.data('threader', new Js8Threader(this));
}
return this.data('threader');
};

55
htdocs/lib/Modes.js Normal file
View File

@ -0,0 +1,55 @@
var Modes = {
modes: [],
features: {},
panels: [],
setModes:function(json){
this.modes = json.map(function(m){ return new Mode(m); });
this.updatePanels();
$('#openwebrx-dialog-bookmark').bookmarkDialog().setModes(this.modes);
},
getModes:function(){
return this.modes;
},
setFeatures:function(features){
this.features = features;
this.updatePanels();
},
findByModulation:function(modulation){
matches = this.modes.filter(function(m) { return m.modulation === modulation; });
if (matches.length) return matches[0]
},
registerModePanel: function(el) {
this.panels.push(el);
},
initComplete: function() {
return this.modes.length && Object.keys(this.features).length;
},
updatePanels: function() {
this.panels.forEach(function(p) {
p.render();
p.startDemodulator();
});
}
};
var Mode = function(json){
this.modulation = json.modulation;
this.name = json.name;
this.type = json.type;
this.requirements = json.requirements;
this.squelch = json.squelch;
if (json.bandpass) {
this.bandpass = json.bandpass;
}
if (this.type === 'digimode') {
this.underlying = json.underlying;
}
};
Mode.prototype.isAvailable = function(){
return this.requirements.map(function(r){
return Modes.features[r];
}).reduce(function(a, b){
return a && b;
}, true);
};

View File

@ -1,10 +1,15 @@
ProgressBar = function(el) { ProgressBar = function(el) {
this.$el = $(el); this.$el = $(el);
this.$innerText = this.$el.find('.openwebrx-progressbar-text'); this.$innerText = $('<span class="openwebrx-progressbar-text">' + this.getDefaultText() + '</span>');
this.$innerBar = this.$el.find('.openwebrx-progressbar-bar'); this.$innerBar = $('<div class="openwebrx-progressbar-bar"></div>');
this.$el.empty().append(this.$innerText, this.$innerBar);
this.$innerBar.css('width', '0%'); this.$innerBar.css('width', '0%');
}; };
ProgressBar.prototype.getDefaultText = function() {
return '';
}
ProgressBar.prototype.set = function(val, text, over) { ProgressBar.prototype.set = function(val, text, over) {
this.setValue(val); this.setValue(val);
this.setText(text); this.setText(text);
@ -25,13 +30,20 @@ ProgressBar.prototype.setOver = function(over) {
this.$innerBar.css('backgroundColor', (over) ? "#ff6262" : "#00aba6"); this.$innerBar.css('backgroundColor', (over) ? "#ff6262" : "#00aba6");
}; };
AudioBufferProgressBar = function(el, sampleRate) { AudioBufferProgressBar = function(el) {
ProgressBar.call(this, el); ProgressBar.call(this, el);
this.sampleRate = sampleRate;
}; };
AudioBufferProgressBar.prototype = new ProgressBar(); AudioBufferProgressBar.prototype = new ProgressBar();
AudioBufferProgressBar.prototype.getDefaultText = function() {
return 'Audio buffer [0 ms]';
};
AudioBufferProgressBar.prototype.setSampleRate = function(sampleRate) {
this.sampleRate = sampleRate;
};
AudioBufferProgressBar.prototype.setBuffersize = function(buffersize) { AudioBufferProgressBar.prototype.setBuffersize = function(buffersize) {
var audio_buffer_value = buffersize / this.sampleRate; var audio_buffer_value = buffersize / this.sampleRate;
var overrun = audio_buffer_value > audio_buffer_maximal_length_sec; var overrun = audio_buffer_value > audio_buffer_maximal_length_sec;
@ -53,6 +65,10 @@ NetworkSpeedProgressBar = function(el) {
NetworkSpeedProgressBar.prototype = new ProgressBar(); NetworkSpeedProgressBar.prototype = new ProgressBar();
NetworkSpeedProgressBar.prototype.getDefaultText = function() {
return 'Network usage [0 kbps]';
};
NetworkSpeedProgressBar.prototype.setSpeed = function(speed) { NetworkSpeedProgressBar.prototype.setSpeed = function(speed) {
var speedInKilobits = speed * 8 / 1000; var speedInKilobits = speed * 8 / 1000;
this.set(speedInKilobits / 2000, "Network usage [" + speedInKilobits.toFixed(1) + " kbps]", false); this.set(speedInKilobits / 2000, "Network usage [" + speedInKilobits.toFixed(1) + " kbps]", false);
@ -64,18 +80,29 @@ AudioSpeedProgressBar = function(el) {
AudioSpeedProgressBar.prototype = new ProgressBar(); AudioSpeedProgressBar.prototype = new ProgressBar();
AudioSpeedProgressBar.prototype.getDefaultText = function() {
return 'Audio stream [0 kbps]';
};
AudioSpeedProgressBar.prototype.setSpeed = function(speed) { AudioSpeedProgressBar.prototype.setSpeed = function(speed) {
this.set(speed / 500000, "Audio stream [" + (speed / 1000).toFixed(0) + " kbps]", false); this.set(speed / 500000, "Audio stream [" + (speed / 1000).toFixed(0) + " kbps]", false);
}; };
AudioOutputProgressBar = function(el, sampleRate) { AudioOutputProgressBar = function(el, sampleRate) {
ProgressBar.call(this, el); ProgressBar.call(this, el);
this.maxRate = sampleRate * 1.25;
this.minRate = sampleRate * .25;
}; };
AudioOutputProgressBar.prototype = new ProgressBar(); AudioOutputProgressBar.prototype = new ProgressBar();
AudioOutputProgressBar.prototype.getDefaultText = function() {
return 'Audio output [0 sps]';
};
AudioOutputProgressBar.prototype.setSampleRate = function(sampleRate) {
this.maxRate = sampleRate * 1.25;
this.minRate = sampleRate * .25;
};
AudioOutputProgressBar.prototype.setAudioRate = function(audioRate) { AudioOutputProgressBar.prototype.setAudioRate = function(audioRate) {
this.set(audioRate / this.maxRate, "Audio output [" + (audioRate / 1000).toFixed(1) + " ksps]", audioRate > this.maxRate || audioRate < this.minRate); this.set(audioRate / this.maxRate, "Audio output [" + (audioRate / 1000).toFixed(1) + " ksps]", audioRate > this.maxRate || audioRate < this.minRate);
}; };
@ -88,6 +115,10 @@ ClientsProgressBar = function(el) {
ClientsProgressBar.prototype = new ProgressBar(); ClientsProgressBar.prototype = new ProgressBar();
ClientsProgressBar.prototype.getDefaultText = function() {
return 'Clients [1]';
};
ClientsProgressBar.prototype.setClients = function(clients) { ClientsProgressBar.prototype.setClients = function(clients) {
this.clients = clients; this.clients = clients;
this.render(); this.render();
@ -108,6 +139,27 @@ CpuProgressBar = function(el) {
CpuProgressBar.prototype = new ProgressBar(); CpuProgressBar.prototype = new ProgressBar();
CpuProgressBar.prototype.getDefaultText = function() {
return 'Server CPU [0%]';
};
CpuProgressBar.prototype.setUsage = function(usage) { CpuProgressBar.prototype.setUsage = function(usage) {
this.set(usage, "Server CPU [" + Math.round(usage * 100) + "%]", usage > .85); this.set(usage, "Server CPU [" + Math.round(usage * 100) + "%]", usage > .85);
}; };
ProgressBar.types = {
cpu: CpuProgressBar,
audiobuffer: AudioBufferProgressBar,
audiospeed: AudioSpeedProgressBar,
audiooutput: AudioOutputProgressBar,
clients: ClientsProgressBar,
networkspeed: NetworkSpeedProgressBar
}
$.fn.progressbar = function() {
if (!this.data('progressbar')) {
var constructor = ProgressBar.types[this.data('type')] || ProgressBar;
this.data('progressbar', new constructor(this));
}
return this.data('progressbar');
};

View File

@ -3,8 +3,10 @@
<head> <head>
<title>OpenWebRX Login</title> <title>OpenWebRX Login</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" /> <link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> <link rel="stylesheet" href="static/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="static/css/login.css" /> <link rel="stylesheet" type="text/css" href="static/css/login.css" />
<script src="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/Header.js"></script>
<meta charset="utf-8"> <meta charset="utf-8">
</head> </head>
<body> <body>
@ -19,7 +21,7 @@
<label for="password">Password</label> <label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Password"> <input type="password" class="form-control" id="password" name="password" placeholder="Password">
</div> </div>
<button type="submit" class="btn btn-login">Login</button> <button type="submit" class="btn btn-secondary btn-login">Login</button>
</form> </form>
</div> </div>
</body> </body>

View File

@ -3,9 +3,7 @@
<head> <head>
<title>OpenWebRX Map</title> <title>OpenWebRX Map</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" /> <link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<script src="static/lib/jquery-3.2.1.min.js"></script> <script src="compiled/map.js"></script>
<script src="static/lib/chroma.min.js"></script>
<script src="static/map.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
<link rel="stylesheet" type="text/css" href="static/css/map.css" /> <link rel="stylesheet" type="text/css" href="static/css/map.css" />
<meta charset="utf-8"> <meta charset="utf-8">

View File

@ -135,7 +135,11 @@
if (expectedCallsign && expectedCallsign == update.callsign.trim()) { if (expectedCallsign && expectedCallsign == update.callsign.trim()) {
map.panTo(pos); map.panTo(pos);
showMarkerInfoWindow(update.callsign, pos); showMarkerInfoWindow(update.callsign, pos);
delete(expectedCallsign); expectedCallsign = false;
}
if (infowindow && infowindow.callsign && infowindow.callsign == update.callsign.trim()) {
showMarkerInfoWindow(infowindow.callsign, pos);
} }
break; break;
case 'locator': case 'locator':
@ -176,7 +180,11 @@
if (expectedLocator && expectedLocator == update.location.locator) { if (expectedLocator && expectedLocator == update.location.locator) {
map.panTo(center); map.panTo(center);
showLocatorInfoWindow(expectedLocator, center); showLocatorInfoWindow(expectedLocator, center);
delete(expectedLocator); expectedLocator = false;
}
if (infowindow && infowindow.locator && infowindow.locator == update.location.locator) {
showLocatorInfoWindow(infowindow.locator, center);
} }
break; break;
} }
@ -250,6 +258,11 @@
case "update": case "update":
processUpdates(json.value); processUpdates(json.value);
break; break;
case 'receiver_details':
$('#webrx-top-container').header().setDetails(json['value']);
break;
default:
console.warn('received message of unknown type: ' + json['type']);
} }
} catch (e) { } catch (e) {
// don't lose exception // don't lose exception
@ -282,9 +295,21 @@
connect(); connect();
var getInfoWindow = function() {
if (!infowindow) {
infowindow = new google.maps.InfoWindow();
google.maps.event.addListener(infowindow, 'closeclick', function() {
delete infowindow.locator;
delete infowindow.callsign;
});
}
return infowindow;
}
var infowindow; var infowindow;
var showLocatorInfoWindow = function(locator, pos) { var showLocatorInfoWindow = function(locator, pos) {
if (!infowindow) infowindow = new google.maps.InfoWindow(); var infowindow = getInfoWindow();
infowindow.locator = locator;
var inLocator = $.map(rectangles, function(r, callsign) { var inLocator = $.map(rectangles, function(r, callsign) {
return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band} return {callsign: callsign, locator: r.locator, lastseen: r.lastseen, mode: r.mode, band: r.band}
}).filter(function(d) { }).filter(function(d) {
@ -310,7 +335,8 @@
}; };
var showMarkerInfoWindow = function(callsign, pos) { var showMarkerInfoWindow = function(callsign, pos) {
if (!infowindow) infowindow = new google.maps.InfoWindow(); var infowindow = getInfoWindow();
infowindow.callsign = callsign;
var marker = markers[callsign]; var marker = markers[callsign];
var timestring = moment(marker.lastseen).fromNow(); var timestring = moment(marker.lastseen).fromNow();
var commentString = ""; var commentString = "";

File diff suppressed because it is too large Load Diff

23
htdocs/sdrsettings.html Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE HTML>
<html>
<head>
<title>OpenWebRX Settings</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="static/css/admin.css" />
<script src="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/Header.js"></script>
<script src="static/settings.js"></script>
<meta charset="utf-8">
</head>
<body>
${header}
<div class="container">
<div class="col-12">
<h1>SDR device settings</h1>
</div>
<div class="col-12">
${devices}
</div>
</div>
</body>

29
htdocs/settings.html Normal file
View File

@ -0,0 +1,29 @@
<!DOCTYPE HTML>
<html>
<head>
<title>OpenWebRX Settings</title>
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico" />
<link rel="stylesheet" href="static/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" href="static/css/admin.css" />
<script src="static/lib/jquery-3.2.1.min.js"></script>
<script src="static/lib/Header.js"></script>
<script src="static/settings.js"></script>
<meta charset="utf-8">
</head>
<body>
${header}
<div class="container">
<div class="col-12">
<h1>Settings</h1>
</div>
<div class="col-12">
<a href="generalsettings">General settings</a>
</div>
<div class="col-12">
<a href="sdrsettings">SDR device settings</a>
</div>
<div class="col-12">
<a href="features">Feature report</a>
</div>
</div>
</body>

View File

@ -1,3 +1,299 @@
function Input(name, value, options) {
this.name = name;
this.value = value;
this.options = options;
this.label = options && options.label || name;
};
Input.prototype.bootstrapify = function(input) {
input.addClass('form-control').addClass('form-control-sm');
return [
'<div class="form-group row">',
'<label class="col-form-label col-form-label-sm col-3" for="' + this.name + '">' + this.label + '</label>',
'<div class="col-9">',
input[0].outerHTML,
'</div>',
'</div>'
].join('');
};
function TextInput() {
Input.apply(this, arguments);
};
TextInput.prototype = new Input();
TextInput.prototype.render = function() {
return this.bootstrapify($('<input type="text" name="' + this.name + '" value="' + this.value + '">'));
}
function NumberInput() {
Input.apply(this, arguments);
};
NumberInput.prototype = new Input();
NumberInput.prototype.render = function() {
return this.bootstrapify($('<input type="number" name="' + this.name + '" value="' + this.value + '">'));
};
function SoapyGainInput() {
Input.apply(this, arguments);
}
SoapyGainInput.prototype = new Input();
SoapyGainInput.prototype.render = function(){
return this.bootstrapify($('<div>Soapy gain settings go here</div>'));
};
function ProfileInput() {
Input.apply(this, arguments);
};
ProfileInput.prototype = new Input();
ProfileInput.prototype.render = function() {
return $('<div><h3>Profiles</h3></div>');
};
function SchedulerInput() {
Input.apply(this, arguments);
};
SchedulerInput.prototype = new Input();
SchedulerInput.prototype.render = function() {
return $('<div><h3>Scheduler</h3></div>');
};
function SdrDevice(el, data) {
this.el = el;
this.data = data;
this.inputs = {};
this.render();
var self = this;
el.on('click', '.fieldselector .btn', function() {
var key = el.find('.fieldselector select').val();
self.data[key] = self.getInitialValue(key);
self.render();
});
};
SdrDevice.create = function(el) {
var data = JSON.parse(decodeURIComponent(el.data('config')));
var type = data.type;
var constructor = SdrDevice.types[type] || SdrDevice;
return new constructor(el, data);
};
SdrDevice.prototype.getData = function() {
return $.extend(new Object(), this.getDefaults(), this.data);
};
SdrDevice.prototype.getDefaults = function() {
var defaults = {}
$.each(this.getMappings(), function(k, v) {
if (!v.includeInDefault) return;
defaults[k] = 'initialValue' in v ? v['initialValue'] : false;
});
return defaults;
};
SdrDevice.prototype.getMappings = function() {
return {
"name": {
constructor: TextInput,
inputOptions: {
label: "Name"
},
initialValue: "",
includeInDefault: true
},
"type": {
constructor: TextInput,
inputOptions: {
label: "Type"
},
initialValue: '',
includeInDefault: true
},
"ppm": {
constructor: NumberInput,
inputOptions: {
label: "PPM"
},
initialValue: 0
},
"profiles": {
constructor: ProfileInput,
inputOptions: {
label: "Profiles"
},
initialValue: [],
includeInDefault: true,
position: 100
},
"scheduler": {
constructor: SchedulerInput,
inputOptions: {
label: "Scheduler",
},
initialValue: {},
position: 101
},
"rf_gain": {
constructor: TextInput,
inputOptions: {
label: "Gain",
},
initialValue: 0
}
};
};
SdrDevice.prototype.getMapping = function(key) {
var mappings = this.getMappings();
return mappings[key];
};
SdrDevice.prototype.getInputClass = function(key) {
var mapping = this.getMapping(key);
return mapping && mapping.constructor || TextInput;
};
SdrDevice.prototype.getInitialValue = function(key) {
var mapping = this.getMapping(key);
return mapping && ('initialValue' in mapping) ? mapping['initialValue'] : false;
};
SdrDevice.prototype.getPosition = function(key) {
var mapping = this.getMapping(key);
return mapping && mapping.position || 10;
};
SdrDevice.prototype.getInputOptions = function(key) {
var mapping = this.getMapping(key);
return mapping && mapping.inputOptions || {};
};
SdrDevice.prototype.getLabel = function(key) {
var options = this.getInputOptions(key);
return options && options.label || key;
};
SdrDevice.prototype.render = function() {
var self = this;
self.el.empty();
var data = this.getData();
Object.keys(data).sort(function(a, b){
return self.getPosition(a) - self.getPosition(b);
}).forEach(function(key){
var value = data[key];
var inputClass = self.getInputClass(key);
var input = new inputClass(key, value, self.getInputOptions(key));
self.inputs[key] = input;
self.el.append(input.render());
});
self.el.append(this.renderFieldSelector());
};
SdrDevice.prototype.renderFieldSelector = function() {
var self = this;
return '<div class="fieldselector">' +
'<h3>Add new configuration options<h3>' +
'<div class="form-group row">' +
'<div class="col-3"><select class="form-control form-control-sm">' +
Object.keys(self.getMappings()).filter(function(m){
return !(m in self.data);
}).map(function(m) {
return '<option value="' + m + '">' + self.getLabel(m) + '</option>';
}).join('') +
'</select></div>' +
'<div class="col-2">' +
'<div class="btn btn-primary">Add to config</div>' +
'</div>' +
'</div>' +
'</div>';
};
RtlSdrDevice = function() {
SdrDevice.apply(this, arguments);
};
RtlSdrDevice.prototype = Object.create(SdrDevice.prototype);
RtlSdrDevice.prototype.constructor = RtlSdrDevice;
RtlSdrDevice.prototype.getMappings = function() {
var mappings = SdrDevice.prototype.getMappings.apply(this, arguments);
return $.extend(new Object(), mappings, {
"device": {
constructor: TextInput,
inputOptions:{
label: "Serial number"
},
initialValue: ""
}
});
};
SoapySdrDevice = function() {
SdrDevice.apply(this, arguments);
};
SoapySdrDevice.prototype = Object.create(SdrDevice.prototype);
SoapySdrDevice.prototype.constructor = SoapySdrDevice;
SoapySdrDevice.prototype.getMappings = function() {
var mappings = SdrDevice.prototype.getMappings.apply(this, arguments);
return $.extend(new Object(), mappings, {
"device": {
constructor: TextInput,
inputOptions:{
label: "Soapy device selector"
},
initialValue: ""
}
});
};
SdrplaySdrDevice = function() {
SoapySdrDevice.apply(this, arguments);
};
SdrplaySdrDevice.prototype = Object.create(SoapySdrDevice.prototype);
SdrplaySdrDevice.prototype.constructor = SdrplaySdrDevice;
SdrplaySdrDevice.prototype.getMappings = function() {
var mappings = SoapySdrDevice.prototype.getMappings.apply(this, arguments);
return $.extend(new Object(), mappings, {
"rf_gain": {
constructor: SoapyGainInput,
initialValue: 0,
inputOptions: {
label: "Gain",
gains: ['RFGR', 'IFGR']
}
}
});
};
SdrDevice.types = {
'rtl_sdr': RtlSdrDevice,
'sdrplay': SdrplaySdrDevice
};
$.fn.sdrdevice = function() {
return this.map(function(){
var el = $(this);
if (!el.data('sdrdevice')) {
el.data('sdrdevice', SdrDevice.create(el));
}
return el.data('sdrdevice');
});
};
$(function(){ $(function(){
$(".map-input").each(function(el) { $(".map-input").each(function(el) {
var $el = $(this); var $el = $(this);
@ -19,5 +315,7 @@ $(function(){
$lon.val(pos.lng); $lon.val(pos.lng);
}); });
}); });
}) });
$(".sdrdevice").sdrdevice();
}); });

View File

@ -1,3 +1,8 @@
import logging
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
from http.server import HTTPServer from http.server import HTTPServer
from owrx.http import RequestHandler from owrx.http import RequestHandler
from owrx.config import Config from owrx.config import Config
@ -10,11 +15,6 @@ from owrx.websocket import WebSocketConnection
from owrx.pskreporter import PskReporter from owrx.pskreporter import PskReporter
from owrx.version import openwebrx_version from owrx.version import openwebrx_version
import logging
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
class ThreadedHttpServer(ThreadingMixIn, HTTPServer): class ThreadedHttpServer(ThreadingMixIn, HTTPServer):
pass pass

244
owrx/audio.py Normal file
View File

@ -0,0 +1,244 @@
from abc import ABC, ABCMeta, abstractmethod
from owrx.config import Config
from owrx.metrics import Metrics, CounterMetric, DirectMetric
import threading
import wave
import subprocess
import os
from multiprocessing.connection import Pipe, wait
from datetime import datetime, timedelta
from queue import Queue, Full
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
class QueueJob(object):
def __init__(self, decoder, file, freq):
self.decoder = decoder
self.file = file
self.freq = freq
def run(self):
self.decoder.decode(self)
def unlink(self):
try:
os.unlink(self.file)
except FileNotFoundError:
pass
class QueueWorker(threading.Thread):
def __init__(self, queue):
self.queue = queue
self.doRun = True
super().__init__(daemon=True)
def run(self) -> None:
while self.doRun:
job = self.queue.get()
try:
job.run()
except Exception:
logger.exception("failed to decode job")
self.queue.onError()
finally:
job.unlink()
self.queue.task_done()
class DecoderQueue(Queue):
sharedInstance = None
creationLock = threading.Lock()
@staticmethod
def getSharedInstance():
with DecoderQueue.creationLock:
if DecoderQueue.sharedInstance is None:
pm = Config.get()
DecoderQueue.sharedInstance = DecoderQueue(maxsize=pm["decoding_queue_length"], workers=pm["decoding_queue_workers"])
return DecoderQueue.sharedInstance
def __init__(self, maxsize, workers):
super().__init__(maxsize)
metrics = Metrics.getSharedInstance()
metrics.addMetric("decoding.queue.length", DirectMetric(self.qsize))
self.inCounter = CounterMetric()
metrics.addMetric("decoding.queue.in", self.inCounter)
self.outCounter = CounterMetric()
metrics.addMetric("decoding.queue.out", self.outCounter)
self.overflowCounter = CounterMetric()
metrics.addMetric("decoding.queue.overflow", self.overflowCounter)
self.errorCounter = CounterMetric()
metrics.addMetric("decoding.queue.error", self.errorCounter)
self.workers = [self.newWorker() for _ in range(0, workers)]
def put(self, item, **kwars):
self.inCounter.inc()
try:
super(DecoderQueue, self).put(item, block=False)
except Full:
self.overflowCounter.inc()
raise
def get(self, **kwargs):
# super.get() is blocking, so it would mess up the stats to inc() first
out = super(DecoderQueue, self).get(**kwargs)
self.outCounter.inc()
return out
def newWorker(self):
worker = QueueWorker(self)
worker.start()
return worker
def onError(self):
self.errorCounter.inc()
class AudioChopperProfile(ABC):
@abstractmethod
def getInterval(self):
pass
@abstractmethod
def getFileTimestampFormat(self):
pass
@abstractmethod
def decoder_commandline(self, file):
pass
class AudioWriter(object):
def __init__(self, dsp, source, profile: AudioChopperProfile):
self.dsp = dsp
self.source = source
self.profile = profile
self.tmp_dir = Config.get()["temporary_directory"]
self.wavefile = None
self.wavefilename = None
self.switchingLock = threading.Lock()
self.timer = None
(self.outputReader, self.outputWriter) = Pipe()
def getWaveFile(self):
filename = "{tmp_dir}/openwebrx-audiochopper-{id}-{timestamp}.wav".format(
tmp_dir=self.tmp_dir,
id=id(self),
timestamp=datetime.utcnow().strftime(self.profile.getFileTimestampFormat()),
)
wavefile = wave.open(filename, "wb")
wavefile.setnchannels(1)
wavefile.setsampwidth(2)
wavefile.setframerate(12000)
return filename, wavefile
def getNextDecodingTime(self):
t = datetime.utcnow()
zeroed = t.replace(minute=0, second=0, microsecond=0)
delta = t - zeroed
interval = self.profile.getInterval()
seconds = (int(delta.total_seconds() / interval) + 1) * interval
t = zeroed + timedelta(seconds=seconds)
logger.debug("scheduling: {0}".format(t))
return t
def cancelTimer(self):
if self.timer:
self.timer.cancel()
self.timer = None
def _scheduleNextSwitch(self):
self.cancelTimer()
delta = self.getNextDecodingTime() - datetime.utcnow()
self.timer = threading.Timer(delta.total_seconds(), self.switchFiles)
self.timer.start()
def switchFiles(self):
self.switchingLock.acquire()
file = self.wavefile
filename = self.wavefilename
(self.wavefilename, self.wavefile) = self.getWaveFile()
self.switchingLock.release()
file.close()
job = QueueJob(self, filename, self.dsp.get_operating_freq())
try:
DecoderQueue.getSharedInstance().put(job)
except Full:
logger.warning("decoding queue overflow; dropping one file")
job.unlink()
self._scheduleNextSwitch()
def decode(self, job: QueueJob):
logger.debug("processing file %s", job.file)
decoder = subprocess.Popen(
["nice", "-n", "10"] + self.profile.decoder_commandline(job.file),
stdout=subprocess.PIPE,
cwd=self.tmp_dir,
close_fds=True,
)
for line in decoder.stdout:
self.outputWriter.send((job.freq, line))
try:
rc = decoder.wait(timeout=10)
if rc != 0:
logger.warning("decoder return code: %i", rc)
except subprocess.TimeoutExpired:
logger.warning("subprocess (pid=%i}) did not terminate correctly; sending kill signal.", decoder.pid)
decoder.kill()
def start(self):
(self.wavefilename, self.wavefile) = self.getWaveFile()
self._scheduleNextSwitch()
def write(self, data):
self.switchingLock.acquire()
self.wavefile.writeframes(data)
self.switchingLock.release()
def stop(self):
self.outputReader.close()
self.outputWriter.close()
self.cancelTimer()
try:
os.unlink(self.wavefilename)
except Exception:
logger.exception("error removing undecoded file")
class AudioChopper(threading.Thread, metaclass=ABCMeta):
def __init__(self, dsp, source, *profiles: AudioChopperProfile):
self.source = source
self.writers = [AudioWriter(dsp, source, p) for p in profiles]
self.doRun = True
super().__init__()
def run(self) -> None:
logger.debug("Audio chopper starting up")
for w in self.writers:
w.start()
while self.doRun:
data = self.source.read(256)
if data is None or (isinstance(data, bytes) and len(data) == 0):
self.doRun = False
else:
for w in self.writers:
w.write(data)
logger.debug("Audio chopper shutting down")
for w in self.writers:
w.stop()
def read(self):
try:
readers = wait([w.outputReader for w in self.writers])
return [r.recv() for r in readers]
except EOFError:
return None

View File

@ -26,6 +26,11 @@ class ConfigMigrator(ABC):
def migrate(self, config): def migrate(self, config):
pass pass
def renameKey(self, config, old, new):
if old in config and not new in config:
config[new] = config[old]
del config[old]
class ConfigMigratorVersion1(ConfigMigrator): class ConfigMigratorVersion1(ConfigMigrator):
def migrate(self, config): def migrate(self, config):
@ -37,6 +42,9 @@ class ConfigMigratorVersion1(ConfigMigrator):
levels = config["waterfall_auto_level_margin"] levels = config["waterfall_auto_level_margin"]
config["waterfall_auto_level_margin"] = {"min": levels[0], "max": levels[1]} config["waterfall_auto_level_margin"] = {"min": levels[0], "max": levels[1]}
self.renameKey(config, "wsjt_queue_workers", "decoding_queue_workers")
self.renameKey(config, "wsjt_queue_length", "decoding_queue_length")
config["version"] = 2 config["version"] = 2
return config return config

View File

@ -1,4 +1,5 @@
from owrx.config import Config from owrx.config import Config
from owrx.details import ReceiverDetails
from owrx.dsp import DspManager from owrx.dsp import DspManager
from owrx.cpu import CpuUsageThread from owrx.cpu import CpuUsageThread
from owrx.sdr import SdrService from owrx.sdr import SdrService
@ -9,10 +10,12 @@ from owrx.version import openwebrx_version
from owrx.bands import Bandplan from owrx.bands import Bandplan
from owrx.bookmarks import Bookmarks from owrx.bookmarks import Bookmarks
from owrx.map import Map from owrx.map import Map
from owrx.locator import Locator
from owrx.property import PropertyStack from owrx.property import PropertyStack
from owrx.modes import Modes, DigitalMode
from multiprocessing import Queue from multiprocessing import Queue
from queue import Full from queue import Full
from js8py import Js8Frame
from abc import ABC, ABCMeta, abstractmethod
import json import json
import threading import threading
@ -21,7 +24,7 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Client(object): class Client(ABC):
def __init__(self, conn): def __init__(self, conn):
self.conn = conn self.conn = conn
self.multiprocessingPipe = Queue(100) self.multiprocessingPipe = Queue(100)
@ -50,6 +53,7 @@ class Client(object):
except Full: except Full:
self.close() self.close()
@abstractmethod
def handleTextMessage(self, conn, message): def handleTextMessage(self, conn, message):
pass pass
@ -60,7 +64,25 @@ class Client(object):
self.close() self.close()
class OpenWebRxReceiverClient(Client): class OpenWebRxClient(Client, metaclass=ABCMeta):
def __init__(self, conn):
super().__init__(conn)
receiver_details = ReceiverDetails()
def send_receiver_info(*args):
receiver_info = receiver_details.__dict__()
self.write_receiver_details(receiver_info)
# TODO unsubscribe
receiver_details.wire(send_receiver_info)
send_receiver_info()
def write_receiver_details(self, details):
self.send({"type": "receiver_details", "value": details})
class OpenWebRxReceiverClient(OpenWebRxClient):
config_keys = [ config_keys = [
"waterfall_colors", "waterfall_colors",
"waterfall_min_level", "waterfall_min_level",
@ -68,7 +90,6 @@ class OpenWebRxReceiverClient(Client):
"waterfall_auto_level_margin", "waterfall_auto_level_margin",
"samp_rate", "samp_rate",
"fft_size", "fft_size",
"fft_fps",
"audio_compression", "audio_compression",
"fft_compression", "fft_compression",
"max_clients", "max_clients",
@ -94,33 +115,16 @@ class OpenWebRxReceiverClient(Client):
self.close() self.close()
raise raise
pm = Config.get()
self.setSdr() self.setSdr()
receiver_details = pm.filter(
"receiver_name",
"receiver_location",
"receiver_asl",
"receiver_gps",
"photo_title",
"photo_desc",
)
def send_receiver_info(*args):
receiver_info = receiver_details.__dict__()
receiver_info["locator"] = Locator.fromCoordinates(receiver_info["receiver_gps"])
self.write_receiver_details(receiver_info)
# TODO unsubscribe
receiver_details.wire(send_receiver_info)
send_receiver_info()
self.__sendProfiles()
features = FeatureDetector().feature_availability() features = FeatureDetector().feature_availability()
self.write_features(features) self.write_features(features)
modes = Modes.getModes()
self.write_modes(modes)
self.__sendProfiles()
CpuUsageThread.getSharedInstance().add_client(self) CpuUsageThread.getSharedInstance().add_client(self)
def __sendProfiles(self): def __sendProfiles(self):
@ -134,14 +138,19 @@ class OpenWebRxReceiverClient(Client):
def handleTextMessage(self, conn, message): def handleTextMessage(self, conn, message):
try: try:
message = json.loads(message) message = json.loads(message)
logger.debug(message)
if "type" in message: if "type" in message:
if message["type"] == "dspcontrol": if message["type"] == "dspcontrol":
if "action" in message and message["action"] == "start": if "action" in message and message["action"] == "start":
self.startDsp() self.startDsp()
if "params" in message: if "params" in message:
params = message["params"] dsp = self.getDsp()
self.setDspProperties(params) if dsp is None:
logger.warning("DSP not available; discarding client data")
else:
params = message["params"]
dsp.setProperties(params)
elif message["type"] == "config": elif message["type"] == "config":
if "params" in message: if "params" in message:
@ -158,7 +167,7 @@ class OpenWebRxReceiverClient(Client):
if "params" in message: if "params" in message:
self.connectionProperties = message["params"] self.connectionProperties = message["params"]
if self.dsp: if self.dsp:
self.setDspProperties(self.connectionProperties) self.getDsp().setProperties(self.connectionProperties)
else: else:
logger.warning("received message without type: {0}".format(message)) logger.warning("received message without type: {0}".format(message))
@ -175,6 +184,7 @@ class OpenWebRxReceiverClient(Client):
next = SdrService.getFirstSource() next = SdrService.getFirstSource()
if next is None: if next is None:
# exit condition: no sdrs available # exit condition: no sdrs available
logger.warning("no more SDR devices available")
self.handleNoSdrsAvailable() self.handleNoSdrsAvailable()
return return
@ -190,16 +200,17 @@ class OpenWebRxReceiverClient(Client):
self.sdr = next self.sdr = next
self.startDsp() self.getDsp()
# keep trying until we find a suitable SDR # found a working sdr, exit the loop
if self.sdr.getState() == SdrSource.STATE_FAILED: if self.sdr.getState() != SdrSource.STATE_FAILED:
self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName()))
else:
break break
logger.warning('SDR device "%s" has failed, selecing new device', self.sdr.getName())
self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName()))
# send initial config # send initial config
self.setDspProperties(self.connectionProperties) self.getDsp().setProperties(self.connectionProperties)
stack = PropertyStack() stack = PropertyStack()
stack.addLayer(0, self.sdr.getProps()) stack.addLayer(0, self.sdr.getProps())
@ -231,9 +242,7 @@ class OpenWebRxReceiverClient(Client):
self.write_sdr_error("No SDR Devices available") self.write_sdr_error("No SDR Devices available")
def startDsp(self): def startDsp(self):
if self.dsp is None and self.sdr is not None: self.getDsp().start()
self.dsp = DspManager(self, self.sdr)
self.dsp.start()
def close(self): def close(self):
self.stopDsp() self.stopDsp()
@ -254,6 +263,8 @@ class OpenWebRxReceiverClient(Client):
def setParams(self, params): def setParams(self, params):
config = Config.get() config = Config.get()
# allow direct configuration only if enabled in the config # allow direct configuration only if enabled in the config
if "configurable_keys" not in config:
return
keys = config["configurable_keys"] keys = config["configurable_keys"]
if not keys: if not keys:
return return
@ -263,11 +274,15 @@ class OpenWebRxReceiverClient(Client):
stack.addLayer(1, config) stack.addLayer(1, config)
protected = stack.filter(*keys) protected = stack.filter(*keys)
for key, value in params.items(): for key, value in params.items():
protected[key] = value try:
protected[key] = value
except KeyError:
pass
def setDspProperties(self, params): def getDsp(self):
for key, value in params.items(): if self.dsp is None and self.sdr is not None:
self.dsp.setProperty(key, value) self.dsp = DspManager(self, self.sdr)
return self.dsp
def write_spectrum_data(self, data): def write_spectrum_data(self, data):
self.mp_send(bytes([0x01]) + data) self.mp_send(bytes([0x01]) + data)
@ -297,9 +312,6 @@ class OpenWebRxReceiverClient(Client):
def write_config(self, cfg): def write_config(self, cfg):
self.send({"type": "config", "value": cfg}) self.send({"type": "config", "value": cfg})
def write_receiver_details(self, details):
self.send({"type": "receiver_details", "value": details})
def write_profiles(self, profiles): def write_profiles(self, profiles):
self.send({"type": "profiles", "value": profiles}) self.send({"type": "profiles", "value": profiles})
@ -333,8 +345,39 @@ class OpenWebRxReceiverClient(Client):
def write_backoff_message(self, reason): def write_backoff_message(self, reason):
self.send({"type": "backoff", "reason": reason}) self.send({"type": "backoff", "reason": reason})
def write_js8_message(self, frame: Js8Frame, freq: int):
self.send({"type": "js8_message", "value": {
"msg": str(frame),
"timestamp": frame.timestamp,
"db": frame.db,
"dt": frame.dt,
"freq": freq + frame.freq,
"thread_type": frame.thread_type,
"mode": frame.mode
}})
class MapConnection(Client): def write_modes(self, modes):
def to_json(m):
res = {
"modulation": m.modulation,
"name": m.name,
"type": "digimode" if isinstance(m, DigitalMode) else "analog",
"requirements": m.requirements,
"squelch": m.squelch,
}
if m.bandpass is not None:
res["bandpass"] = {
"low_cut": m.bandpass.low_cut,
"high_cut": m.bandpass.high_cut
}
if isinstance(m, DigitalMode):
res["underlying"] = m.underlying
return res
self.send({"type": "modes", "value": [to_json(m) for m in modes]})
class MapConnection(OpenWebRxClient):
def __init__(self, conn): def __init__(self, conn):
super().__init__(conn) super().__init__(conn)

View File

@ -1,6 +1,11 @@
from .template import WebpageController from .template import WebpageController
from .session import SessionStorage from .session import SessionStorage
from owrx.config import Config from owrx.config import Config
from urllib import parse
import logging
logger = logging.getLogger(__name__)
class Authentication(object): class Authentication(object):
@ -18,10 +23,11 @@ class AdminController(WebpageController):
def handle_request(self): def handle_request(self):
config = Config.get() config = Config.get()
if not config["webadmin_enabled"]: if "webadmin_enabled" not in config or not config["webadmin_enabled"]:
self.send_response("Web Admin is disabled", code=403) self.send_response("Web Admin is disabled", code=403)
return return
if self.authentication.isAuthenticated(self.request): if self.authentication.isAuthenticated(self.request):
super().handle_request() super().handle_request()
else: else:
self.send_redirect("/login") target = "/login?{0}".format(parse.urlencode({"ref": self.request.path}))
self.send_redirect(target)

View File

@ -1,5 +1,6 @@
from . import Controller from . import Controller
from owrx.feature import FeatureDetector from owrx.feature import FeatureDetector
from owrx.details import ReceiverDetails
import json import json
@ -7,3 +8,8 @@ class ApiController(Controller):
def indexAction(self): def indexAction(self):
data = json.dumps(FeatureDetector().feature_report()) data = json.dumps(FeatureDetector().feature_report())
self.send_response(data, content_type="application/json") self.send_response(data, content_type="application/json")
def receiverDetails(self):
receiver_details = ReceiverDetails()
data = json.dumps(receiver_details.__dict__())
self.send_response(data, content_type="application/json")

View File

@ -4,13 +4,18 @@ from datetime import datetime
import mimetypes import mimetypes
import os import os
import pkg_resources import pkg_resources
from abc import ABCMeta, abstractmethod
class AssetsController(Controller): class AssetsController(Controller, metaclass=ABCMeta):
def getModified(self, file): def getModified(self, file):
return None return datetime.fromtimestamp(os.path.getmtime(self.getFilePath(file)))
def openFile(self, file): def openFile(self, file):
return open(self.getFilePath(file), "rb")
@abstractmethod
def getFilePath(self, file):
pass pass
def serve_file(self, file, content_type=None): def serve_file(self, file, content_type=None):
@ -41,8 +46,8 @@ class AssetsController(Controller):
class OwrxAssetsController(AssetsController): class OwrxAssetsController(AssetsController):
def openFile(self, file): def getFilePath(self, file):
return pkg_resources.resource_stream("htdocs", file) return pkg_resources.resource_filename("htdocs", file)
class AprsSymbolsController(AssetsController): class AprsSymbolsController(AssetsController):
@ -57,8 +62,61 @@ class AprsSymbolsController(AssetsController):
def getFilePath(self, file): def getFilePath(self, file):
return self.path + file return self.path + file
def getModified(self, file):
return datetime.fromtimestamp(os.path.getmtime(self.getFilePath(file)))
def openFile(self, file): class CompiledAssetsController(Controller):
return open(self.getFilePath(file), "rb") profiles = {
"receiver.js": [
"openwebrx.js",
"lib/jquery-3.2.1.min.js",
"lib/jquery.nanoscroller.js",
"lib/Header.js",
"lib/Demodulator.js",
"lib/DemodulatorPanel.js",
"lib/BookmarkBar.js",
"lib/BookmarkDialog.js",
"lib/AudioEngine.js",
"lib/ProgressBar.js",
"lib/Measurement.js",
"lib/FrequencyDisplay.js",
"lib/Js8Threads.js",
"lib/Modes.js",
],
"map.js": [
"lib/jquery-3.2.1.min.js",
"lib/chroma.min.js",
"lib/Header.js",
"map.js",
],
}
def indexAction(self):
profileName = self.request.matches.group(1)
if profileName not in CompiledAssetsController.profiles:
self.send_response("profile not found", code=404)
return
files = CompiledAssetsController.profiles[profileName]
files = [pkg_resources.resource_filename("htdocs", f) for f in files]
modified = self.getModified(files)
if modified is not None and "If-Modified-Since" in self.handler.headers:
client_modified = datetime.strptime(
self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z"
)
if modified <= client_modified:
self.send_response("", code=304)
return
contents = [self.getContents(f) for f in files]
(content_type, encoding) = mimetypes.MimeTypes().guess_type(profileName)
self.send_response("\n".join(contents), content_type=content_type, last_modified=modified, max_age=3600)
def getContents(self, file):
with open(file) as f:
return f.read()
def getModified(self, files):
modified = [datetime.fromtimestamp(os.path.getmtime(f)) for f in files]
return max(*modified)

View File

@ -46,12 +46,12 @@ class SessionController(WebpageController):
if data["user"] in userlist: if data["user"] in userlist:
user = userlist[data["user"]] user = userlist[data["user"]]
if user.password.is_valid(data["password"]): if user.password.is_valid(data["password"]):
# TODO pass the final destination
# TODO evaluate password force_change and redirect to password change # TODO evaluate password force_change and redirect to password change
key = SessionStorage.getSharedInstance().startSession({"user": user.name}) key = SessionStorage.getSharedInstance().startSession({"user": user.name})
cookie = SimpleCookie() cookie = SimpleCookie()
cookie["owrx-session"] = key cookie["owrx-session"] = key
self.send_redirect("/admin", cookies=cookie) target = self.request.query["ref"][0] if "ref" in self.request.query else "/settings"
self.send_redirect(target, cookies=cookie)
return return
self.send_redirect("/login") self.send_redirect("/login")

View File

@ -11,7 +11,10 @@ from owrx.form import (
DropdownInput, DropdownInput,
Option, Option,
ServicesCheckboxInput, ServicesCheckboxInput,
Js8ProfileCheckboxInput,
) )
from urllib.parse import quote
import json
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -43,6 +46,41 @@ class Section(object):
class SettingsController(AdminController): class SettingsController(AdminController):
def indexAction(self):
self.serve_template("settings.html", **self.template_variables())
class SdrSettingsController(AdminController):
def template_variables(self):
variables = super().template_variables()
variables["devices"] = self.render_devices()
return variables
def render_devices(self):
return "".join(self.render_device(key, value) for key, value in Config.get()["sdrs"].items())
def render_device(self, device_id, config):
return """
<div class="card device bg-dark text-white">
<div class="card-header">
{device_name}
</div>
<div class="card-body">
{form}
</div>
</div>
""".format(device_name=config["name"], form=self.render_form(device_id, config))
def render_form(self, device_id, config):
return """
<form class="sdrdevice" data-config="{formdata}"></form>
""".format(device_id=device_id, formdata=quote(json.dumps(config)))
def indexAction(self):
self.serve_template("sdrsettings.html", **self.template_variables())
class GeneralSettingsController(AdminController):
sections = [ sections = [
Section( Section(
"General settings", "General settings",
@ -145,14 +183,23 @@ class SettingsController(AdminController):
), ),
), ),
Section( Section(
"WSJT-X settings", "Decoding settings",
NumberInput("wsjt_queue_workers", "Number of WSJT decoding workers"), NumberInput("decoding_queue_workers", "Number of decoding workers"),
NumberInput("wsjt_queue_length", "Maximum length of WSJT job queue"), NumberInput("decoding_queue_length", "Maximum length of decoding job queue"),
NumberInput( NumberInput(
"wsjt_decoding_depth", "wsjt_decoding_depth",
"WSJT decoding depth", "Default WSJT decoding depth",
infotext="A higher decoding depth will allow more results, but will also consume more cpu", infotext="A higher decoding depth will allow more results, but will also consume more cpu",
), ),
NumberInput(
"js8_decoding_depth",
"Js8Call decoding depth",
infotext="A higher decoding depth will allow more results, but will also consume more cpu",
),
Js8ProfileCheckboxInput(
"js8_enabled_profiles",
"Js8Call enabled modes"
),
), ),
Section( Section(
"Background decoding", "Background decoding",
@ -212,7 +259,7 @@ class SettingsController(AdminController):
] ]
def render_sections(self): def render_sections(self):
sections = "".join(section.render() for section in SettingsController.sections) sections = "".join(section.render() for section in GeneralSettingsController.sections)
return """ return """
<form class="settings-body" method="POST"> <form class="settings-body" method="POST">
{sections} {sections}
@ -225,7 +272,7 @@ class SettingsController(AdminController):
) )
def indexAction(self): def indexAction(self):
self.serve_template("admin.html", **self.template_variables()) self.serve_template("generalsettings.html", **self.template_variables())
def template_variables(self): def template_variables(self):
variables = super().template_variables() variables = super().template_variables()
@ -235,7 +282,7 @@ class SettingsController(AdminController):
def processFormData(self): def processFormData(self):
data = parse_qs(self.get_body().decode("utf-8")) data = parse_qs(self.get_body().decode("utf-8"))
data = { data = {
k: v for i in SettingsController.sections for k, v in i.parse(data).items() k: v for i in GeneralSettingsController.sections for k, v in i.parse(data).items()
} }
config = Config.get() config = Config.get()
for k, v in data.items(): for k, v in data.items():

View File

@ -5,6 +5,7 @@ from owrx.sdr import SdrService
from owrx.config import Config from owrx.config import Config
import os import os
import json import json
import pkg_resources
class StatusController(Controller): class StatusController(Controller):
@ -12,6 +13,7 @@ class StatusController(Controller):
pm = Config.get() pm = Config.get()
# convert to old format # convert to old format
gps = (pm["receiver_gps"]["lat"], pm["receiver_gps"]["lon"]) gps = (pm["receiver_gps"]["lat"], pm["receiver_gps"]["lon"])
avatar_path = pkg_resources.resource_filename("htdocs", "gfx/openwebrx-avatar.png")
# TODO keys that have been left out since they are no longer simple strings: sdr_hw, bands, antenna # TODO keys that have been left out since they are no longer simple strings: sdr_hw, bands, antenna
vars = { vars = {
"status": "active", "status": "active",
@ -23,7 +25,7 @@ class StatusController(Controller):
"asl": pm["receiver_asl"], "asl": pm["receiver_asl"],
"loc": pm["receiver_location"], "loc": pm["receiver_location"],
"sw_version": openwebrx_version, "sw_version": openwebrx_version,
"avatar_ctime": os.path.getctime("htdocs/gfx/openwebrx-avatar.png"), "avatar_ctime": os.path.getctime(avatar_path),
} }
self.send_response("\n".join(["{key}={value}".format(key=key, value=value) for key, value in vars.items()])) self.send_response("\n".join(["{key}={value}".format(key=key, value=value) for key, value in vars.items()]))

View File

@ -1,6 +1,7 @@
from . import Controller from . import Controller
import pkg_resources import pkg_resources
from string import Template from string import Template
from owrx.config import Config
class TemplateController(Controller): class TemplateController(Controller):
@ -19,7 +20,11 @@ class TemplateController(Controller):
class WebpageController(TemplateController): class WebpageController(TemplateController):
def template_variables(self): def template_variables(self):
header = self.render_template("include/header.include.html") settingslink = ""
pm = Config.get()
if "webadmin_enabled" in pm and pm["webadmin_enabled"]:
settingslink = """<a class="button" href="settings" target="openwebrx-settings"><img src="static/gfx/openwebrx-panel-settings.png" alt="Settings"/><br/>Settings</a>"""
header = self.render_template("include/header.include.html", settingslink=settingslink)
return {"header": header} return {"header": header}

21
owrx/details.py Normal file
View File

@ -0,0 +1,21 @@
from owrx.config import Config
from owrx.locator import Locator
from owrx.property import PropertyFilter
class ReceiverDetails(PropertyFilter):
def __init__(self):
super().__init__(
Config.get(),
"receiver_name",
"receiver_location",
"receiver_asl",
"receiver_gps",
"photo_title",
"photo_desc",
)
def __dict__(self):
receiver_info = super().__dict__()
receiver_info["locator"] = Locator.fromCoordinates(receiver_info["receiver_gps"])
return receiver_info

View File

@ -1,10 +1,11 @@
from owrx.config import Config
from owrx.meta import MetaParser from owrx.meta import MetaParser
from owrx.wsjt import WsjtParser from owrx.wsjt import WsjtParser
from owrx.js8 import Js8Parser
from owrx.aprs import AprsParser from owrx.aprs import AprsParser
from owrx.pocsag import PocsagParser from owrx.pocsag import PocsagParser
from owrx.source import SdrSource from owrx.source import SdrSource
from owrx.property import PropertyStack, PropertyLayer from owrx.property import PropertyStack, PropertyLayer
from owrx.modes import Modes
from csdr import csdr from csdr import csdr
import threading import threading
@ -22,6 +23,7 @@ class DspManager(csdr.output):
"wsjt_demod": WsjtParser(self.handler), "wsjt_demod": WsjtParser(self.handler),
"packet_demod": AprsParser(self.handler), "packet_demod": AprsParser(self.handler),
"pocsag_demod": PocsagParser(self.handler), "pocsag_demod": PocsagParser(self.handler),
"js8_demod": Js8Parser(self.handler),
} }
self.props = PropertyStack() self.props = PropertyStack()
@ -35,6 +37,7 @@ class DspManager(csdr.output):
"offset_freq", "offset_freq",
"mod", "mod",
"secondary_offset_freq", "secondary_offset_freq",
"dmr_filter",
)) ))
# properties that we inherit from the sdr # properties that we inherit from the sdr
self.props.addLayer(1, self.sdrSource.getProps().filter( self.props.addLayer(1, self.sdrSource.getProps().filter(
@ -47,9 +50,10 @@ class DspManager(csdr.output):
"digimodes_enable", "digimodes_enable",
"samp_rate", "samp_rate",
"digital_voice_unvoiced_quality", "digital_voice_unvoiced_quality",
"dmr_filter",
"temporary_directory", "temporary_directory",
"center_freq", "center_freq",
"start_mod",
"start_freq",
)) ))
self.dsp = csdr.dsp(self) self.dsp = csdr.dsp(self)
@ -70,6 +74,20 @@ class DspManager(csdr.output):
for parser in self.parsers.values(): for parser in self.parsers.values():
parser.setDialFrequency(freq) parser.setDialFrequency(freq)
if "start_mod" in self.props:
self.dsp.set_demodulator(self.props["start_mod"])
mode = Modes.findByModulation(self.props["start_mod"])
if mode and mode.bandpass:
self.dsp.set_bpf(mode.bandpass.low_cut, mode.bandpass.high_cut)
else:
self.dsp.set_bpf(-4000, 4000)
if "start_freq" in self.props and "center_freq" in self.props:
self.dsp.set_offset_freq(self.props["start_freq"] - self.props["center_freq"])
else:
self.dsp.set_offset_freq(0)
self.subscriptions = [ self.subscriptions = [
self.props.wireProperty("audio_compression", self.dsp.set_audio_compression), self.props.wireProperty("audio_compression", self.dsp.set_audio_compression),
self.props.wireProperty("fft_compression", self.dsp.set_fft_compression), self.props.wireProperty("fft_compression", self.dsp.set_fft_compression),
@ -88,8 +106,6 @@ class DspManager(csdr.output):
self.props.filter("center_freq", "offset_freq").wire(set_dial_freq), self.props.filter("center_freq", "offset_freq").wire(set_dial_freq),
] ]
self.dsp.set_offset_freq(0)
self.dsp.set_bpf(-4000, 4000)
self.dsp.csdr_dynamic_bufsize = self.props["csdr_dynamic_bufsize"] self.dsp.csdr_dynamic_bufsize = self.props["csdr_dynamic_bufsize"]
self.dsp.csdr_print_bufsizes = self.props["csdr_print_bufsizes"] self.dsp.csdr_print_bufsizes = self.props["csdr_print_bufsizes"]
self.dsp.csdr_through = self.props["csdr_through"] self.dsp.csdr_through = self.props["csdr_through"]
@ -114,6 +130,8 @@ class DspManager(csdr.output):
self.props.wireProperty("secondary_offset_freq", self.dsp.set_secondary_offset_freq), self.props.wireProperty("secondary_offset_freq", self.dsp.set_secondary_offset_freq),
] ]
self.startOnAvailable = False
self.sdrSource.addClient(self) self.sdrSource.addClient(self)
super().__init__() super().__init__()
@ -121,6 +139,8 @@ class DspManager(csdr.output):
def start(self): def start(self):
if self.sdrSource.isAvailable(): if self.sdrSource.isAvailable():
self.dsp.start() self.dsp.start()
else:
self.startOnAvailable = True
def receive_output(self, t, read_fn): def receive_output(self, t, read_fn):
logger.debug("adding new output of type %s", t) logger.debug("adding new output of type %s", t)
@ -139,11 +159,16 @@ class DspManager(csdr.output):
def stop(self): def stop(self):
self.dsp.stop() self.dsp.stop()
self.startOnAvailable = False
self.sdrSource.removeClient(self) self.sdrSource.removeClient(self)
for sub in self.subscriptions: for sub in self.subscriptions:
sub.cancel() sub.cancel()
self.subscriptions = [] self.subscriptions = []
def setProperties(self, props):
for k, v in props.items():
self.setProperty(k, v)
def setProperty(self, prop, value): def setProperty(self, prop, value):
self.props[prop] = value self.props[prop] = value
@ -153,7 +178,9 @@ class DspManager(csdr.output):
def onStateChange(self, state): def onStateChange(self, state):
if state == SdrSource.STATE_RUNNING: if state == SdrSource.STATE_RUNNING:
logger.debug("received STATE_RUNNING, attempting DspSource restart") logger.debug("received STATE_RUNNING, attempting DspSource restart")
self.dsp.start() if self.startOnAvailable:
self.dsp.start()
self.startOnAvailable = False
elif state == SdrSource.STATE_STOPPING: elif state == SdrSource.STATE_STOPPING:
logger.debug("received STATE_STOPPING, shutting down DspSource") logger.debug("received STATE_STOPPING, shutting down DspSource")
self.dsp.stop() self.dsp.stop()

View File

@ -29,7 +29,7 @@ class FeatureDetector(object):
"airspy": ["soapy_connector", "soapy_airspy"], "airspy": ["soapy_connector", "soapy_airspy"],
"airspyhf": ["soapy_connector", "soapy_airspyhf"], "airspyhf": ["soapy_connector", "soapy_airspyhf"],
"lime_sdr": ["soapy_connector", "soapy_lime_sdr"], "lime_sdr": ["soapy_connector", "soapy_lime_sdr"],
"fifi_sdr": ["alsa"], "fifi_sdr": ["alsa", "rockprog"],
"pluto_sdr": ["soapy_connector", "soapy_pluto_sdr"], "pluto_sdr": ["soapy_connector", "soapy_pluto_sdr"],
"soapy_remote": ["soapy_connector", "soapy_remote"], "soapy_remote": ["soapy_connector", "soapy_remote"],
"uhd": ["soapy_connector", "soapy_uhd"], "uhd": ["soapy_connector", "soapy_uhd"],
@ -40,6 +40,7 @@ class FeatureDetector(object):
"wsjt-x": ["wsjtx", "sox"], "wsjt-x": ["wsjtx", "sox"],
"packet": ["direwolf", "sox"], "packet": ["direwolf", "sox"],
"pocsag": ["digiham", "sox"], "pocsag": ["digiham", "sox"],
"js8call": ["js8", "sox"],
} }
def feature_availability(self): def feature_availability(self):
@ -246,13 +247,13 @@ class FeatureDetector(object):
def _has_soapy_driver(self, driver): def _has_soapy_driver(self, driver):
try: try:
process = subprocess.Popen(["SoapySDRUtil", "--info"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) process = subprocess.Popen(["SoapySDRUtil", "--info"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
factory_regex = re.compile("^Available factories\\.\\.\\. (.*)$") factory_regex = re.compile("^Available factories\\.\\.\\. ?(.*)$")
drivers = [] drivers = []
for line in process.stdout: for line in process.stdout:
matches = factory_regex.match(line.decode()) matches = factory_regex.match(line.decode())
if matches: if matches:
drivers = [s.strip() for s in matches[1].split(", ")] drivers = [s.strip() for s in matches.group(1).split(", ")]
return driver in drivers return driver in drivers
except FileNotFoundError: except FileNotFoundError:
@ -370,9 +371,27 @@ class FeatureDetector(object):
""" """
return reduce(and_, map(self.command_is_runnable, ["jt9", "wsprd"]), True) return reduce(and_, map(self.command_is_runnable, ["jt9", "wsprd"]), True)
def has_js8(self):
"""
To decode JS8, you will need to install [JS8Call](http://js8call.com/)
Please note that the `js8` command line decoder is not made available on $PATH by some JS8Call package builds.
You will need to manually make it available by either linking it to `/usr/bin` or by adding its location to
$PATH.
"""
return self.command_is_runnable("js8")
def has_alsa(self): def has_alsa(self):
""" """
Some SDR receivers are identifying themselves as a soundcard. In order to read their data, OpenWebRX relies Some SDR receivers are identifying themselves as a soundcard. In order to read their data, OpenWebRX relies
on the Alsa library. It is available as a package for most Linux distributions. on the Alsa library. It is available as a package for most Linux distributions.
""" """
return self.command_is_runnable("arecord --help") return self.command_is_runnable("arecord --help")
def has_rockprog(self):
"""
The "rockprog" executable is required to send commands to your FiFiSDR. It needs to be installed separately.
You can find instructions and downloads [here](https://o28.sischa.net/fifisdr/trac/wiki/De%3Arockprog).
"""
return self.command_is_runnable("rockprog")

View File

@ -1,5 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from owrx.service import ServiceDetector from owrx.modes import Modes
from owrx.config import Config from owrx.config import Config
@ -196,11 +196,22 @@ class MultiCheckboxInput(Input):
class ServicesCheckboxInput(MultiCheckboxInput): class ServicesCheckboxInput(MultiCheckboxInput):
def __init__(self, id, label, infotext=None): def __init__(self, id, label, infotext=None):
services = [ services = [
Option(s, s.upper()) for s in ServiceDetector.getAvailableServices() Option(s.modulation, s.name) for s in Modes.getAvailableServices()
] ]
super().__init__(id, label, services, infotext) super().__init__(id, label, services, infotext)
class Js8ProfileCheckboxInput(MultiCheckboxInput):
def __init__(self, id, label, infotext=None):
profiles = [
Option("normal", "Normal (15s, 50Hz, ~16WPM)"),
Option("slow", "Slow (30s, 25Hz, ~8WPM"),
Option("fast", "Fast (10s, 80Hz, ~24WPM"),
Option("turbo", "Turbo (6s, 160Hz, ~40WPM"),
]
super().__init__(id, label, profiles, infotext)
class DropdownInput(Input): class DropdownInput(Input):
def __init__(self, id, label, options, infotext=None): def __init__(self, id, label, options, infotext=None):
super().__init__(id, label, infotext=infotext) super().__init__(id, label, infotext=infotext)

View File

@ -6,12 +6,13 @@ from owrx.controllers.template import (
) )
from owrx.controllers.assets import ( from owrx.controllers.assets import (
OwrxAssetsController, OwrxAssetsController,
AprsSymbolsController AprsSymbolsController,
CompiledAssetsController
) )
from owrx.controllers.websocket import WebSocketController from owrx.controllers.websocket import WebSocketController
from owrx.controllers.api import ApiController from owrx.controllers.api import ApiController
from owrx.controllers.metrics import MetricsController from owrx.controllers.metrics import MetricsController
from owrx.controllers.settings import SettingsController from owrx.controllers.settings import SettingsController, GeneralSettingsController, SdrSettingsController
from owrx.controllers.session import SessionController from owrx.controllers.session import SessionController
from http.server import BaseHTTPRequestHandler from http.server import BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
@ -91,6 +92,7 @@ class Router(object):
StaticRoute("/status", StatusController), StaticRoute("/status", StatusController),
StaticRoute("/status.json", StatusController, options={"action": "jsonAction"}), StaticRoute("/status.json", StatusController, options={"action": "jsonAction"}),
RegexRoute("/static/(.+)", OwrxAssetsController), RegexRoute("/static/(.+)", OwrxAssetsController),
RegexRoute("/compiled/(.+)", CompiledAssetsController),
RegexRoute("/aprs-symbols/(.+)", AprsSymbolsController), RegexRoute("/aprs-symbols/(.+)", AprsSymbolsController),
StaticRoute("/ws/", WebSocketController), StaticRoute("/ws/", WebSocketController),
RegexRoute("(/favicon.ico)", OwrxAssetsController), RegexRoute("(/favicon.ico)", OwrxAssetsController),
@ -99,9 +101,12 @@ class Router(object):
StaticRoute("/map", MapController), StaticRoute("/map", MapController),
StaticRoute("/features", FeatureController), StaticRoute("/features", FeatureController),
StaticRoute("/api/features", ApiController), StaticRoute("/api/features", ApiController),
StaticRoute("/api/receiverdetails", ApiController, options={"action": "receiverDetails"}),
StaticRoute("/metrics", MetricsController), StaticRoute("/metrics", MetricsController),
StaticRoute("/admin", SettingsController), StaticRoute("/settings", SettingsController),
StaticRoute("/admin", SettingsController, method="POST", options={"action": "processFormData"}), StaticRoute("/generalsettings", GeneralSettingsController),
StaticRoute("/generalsettings", GeneralSettingsController, method="POST", options={"action": "processFormData"}),
StaticRoute("/sdrsettings", SdrSettingsController),
StaticRoute("/login", SessionController, options={"action": "loginAction"}), StaticRoute("/login", SessionController, options={"action": "loginAction"}),
StaticRoute("/login", SessionController, method="POST", options={"action": "processLoginAction"}), StaticRoute("/login", SessionController, method="POST", options={"action": "processLoginAction"}),
StaticRoute("/logout", SessionController, options={"action": "logoutAction"}), StaticRoute("/logout", SessionController, options={"action": "logoutAction"}),

132
owrx/js8.py Normal file
View File

@ -0,0 +1,132 @@
from .audio import AudioChopperProfile
from .parser import Parser
import re
from js8py import Js8
from js8py.frames import Js8FrameHeartbeat, Js8FrameCompound
from .map import Map, LocatorLocation
from .pskreporter import PskReporter
from .metrics import Metrics, CounterMetric
from .config import Config
from abc import ABCMeta, abstractmethod
import logging
logger = logging.getLogger(__name__)
class Js8Profiles(object):
@staticmethod
def getEnabledProfiles():
config = Config.get()
profiles = config["js8_enabled_profiles"] if "js8_enabled_profiles" in config else []
return [Js8Profiles.loadProfile(p) for p in profiles]
@staticmethod
def loadProfile(profileName):
className = "Js8{0}Profile".format(profileName[0].upper() + profileName[1:].lower())
return globals()[className]()
class Js8Profile(AudioChopperProfile, metaclass=ABCMeta):
def decoding_depth(self, mode):
pm = Config.get()
# return global default
if "js8_decoding_depth" in pm:
return pm["js8_decoding_depth"]
# default when no setting is provided
return 3
def getFileTimestampFormat(self):
return "%y%m%d_%H%M%S"
def decoder_commandline(self, file):
return ["js8", "--js8", "-b", self.get_sub_mode(), "-d", str(self.decoding_depth("js8")), file]
@abstractmethod
def get_sub_mode(self):
pass
class Js8NormalProfile(Js8Profile):
def getInterval(self):
return 15
def get_sub_mode(self):
return "A"
class Js8SlowProfile(Js8Profile):
def getInterval(self):
return 30
def get_sub_mode(self):
return "E"
class Js8FastProfile(Js8Profile):
def getInterval(self):
return 10
def get_sub_mode(self):
return "B"
class Js8TurboProfile(Js8Profile):
def getInterval(self):
return 6
def get_sub_mode(self):
return "C"
class Js8Parser(Parser):
decoderRegex = re.compile(" ?<Decode(Started|Debug|Finished)>")
def parse(self, messages):
for raw in messages:
try:
freq, raw_msg = raw
self.setDialFrequency(freq)
msg = raw_msg.decode().rstrip()
if Js8Parser.decoderRegex.match(msg):
return
if msg.startswith(" EOF on input file"):
return
frame = Js8().parse_message(msg)
self.handler.write_js8_message(frame, self.dial_freq)
self.pushDecode()
if (isinstance(frame, Js8FrameHeartbeat) or isinstance(frame, Js8FrameCompound)) and frame.grid:
Map.getSharedInstance().updateLocation(
frame.callsign, LocatorLocation(frame.grid), "JS8", self.band
)
PskReporter.getSharedInstance().spot({
"callsign": frame.callsign,
"mode": "JS8",
"locator": frame.grid,
"freq": self.dial_freq + frame.freq,
"db": frame.db,
"timestamp": frame.timestamp,
"msg": str(frame)
})
except Exception:
logger.exception("error while parsing js8 message")
def pushDecode(self):
metrics = Metrics.getSharedInstance()
band = "unknown"
if self.band is not None:
band = self.band.getName()
if band is None:
band = "unknown"
name = "js8call.decodes.{band}.JS8".format(band=band)
metric = metrics.getMetric(name)
if metric is None:
metric = CounterMetric()
metrics.addMetric(name, metric)
metric.inc()

90
owrx/modes.py Normal file
View File

@ -0,0 +1,90 @@
from owrx.feature import FeatureDetector
from functools import reduce
class Bandpass(object):
def __init__(self, low_cut, high_cut):
self.low_cut = low_cut
self.high_cut = high_cut
class Mode(object):
def __init__(self, modulation, name, bandpass: Bandpass = None, requirements=None, service=False, squelch=True):
self.modulation = modulation
self.name = name
self.requirements = requirements if requirements is not None else []
self.service = service
self.bandpass = bandpass
self.squelch = squelch
def is_available(self):
fd = FeatureDetector()
return reduce(lambda a, b: a and b, [fd.is_available(r) for r in self.requirements], True)
def is_service(self):
return self.service
class AnalogMode(Mode):
pass
class DigitalMode(Mode):
def __init__(
self, modulation, name, underlying, bandpass: Bandpass = None, requirements=None, service=False, squelch=True
):
super().__init__(modulation, name, bandpass, requirements, service, squelch)
self.underlying = underlying
class Modes(object):
mappings = [
AnalogMode("nfm", "FM", bandpass=Bandpass(-4000, 4000)),
AnalogMode("am", "AM", bandpass=Bandpass(-4000, 4000)),
AnalogMode("lsb", "LSB", bandpass=Bandpass(-3000, -300)),
AnalogMode("usb", "USB", bandpass=Bandpass(300, 3000)),
AnalogMode("cw", "CW", bandpass=Bandpass(700, 900)),
AnalogMode("dmr", "DMR", bandpass=Bandpass(-4000, 4000), requirements=["digital_voice_digiham"], squelch=False),
AnalogMode("dstar", "DStar", bandpass=Bandpass(-3250, 3250), requirements=["digital_voice_dsd"], squelch=False),
AnalogMode("nxdn", "NXDN", bandpass=Bandpass(-3250, 3250), requirements=["digital_voice_dsd"], squelch=False),
AnalogMode("ysf", "YSF", bandpass=Bandpass(-4000, 4000), requirements=["digital_voice_digiham"], squelch=False),
DigitalMode("bpsk31", "BPSK31", underlying=["usb"]),
DigitalMode("bpsk63", "BPSK63", underlying=["usb"]),
DigitalMode("ft8", "FT8", underlying=["usb"], requirements=["wsjt-x"], service=True),
DigitalMode("ft4", "FT4", underlying=["usb"], requirements=["wsjt-x"], service=True),
DigitalMode("jt65", "JT65", underlying=["usb"], requirements=["wsjt-x"], service=True),
DigitalMode("jt9", "JT9", underlying=["usb"], requirements=["wsjt-x"], service=True),
DigitalMode(
"wspr", "WSPR", underlying=["usb"], bandpass=Bandpass(1350, 1650), requirements=["wsjt-x"], service=True
),
DigitalMode("js8", "JS8Call", underlying=["usb"], requirements=["js8call"], service=True),
DigitalMode(
"packet", "Packet", underlying=["nfm", "usb", "lsb"], requirements=["packet"], service=True, squelch=False
),
DigitalMode(
"pocsag",
"Pocsag",
underlying=["nfm"],
bandpass=Bandpass(-6000, 6000),
requirements=["pocsag"],
squelch=False,
),
]
@staticmethod
def getModes():
return Modes.mappings
@staticmethod
def getAvailableModes():
return [m for m in Modes.getModes() if m.is_available()]
@staticmethod
def getAvailableServices():
return [m for m in Modes.getAvailableModes() if m.is_service()]
@staticmethod
def findByModulation(modulation):
modes = [m for m in Modes.getAvailableModes() if m.modulation == modulation]
if modes:
return modes[0]

View File

@ -40,6 +40,10 @@ class PropertyManager(ABC):
def __dict__(self): def __dict__(self):
pass pass
@abstractmethod
def __delitem__(self, key):
pass
@abstractmethod @abstractmethod
def keys(self): def keys(self):
pass pass
@ -98,6 +102,9 @@ class PropertyLayer(PropertyManager):
def __dict__(self): def __dict__(self):
return {k: v for k, v in self.properties.items()} return {k: v for k, v in self.properties.items()}
def __delitem__(self, key):
return self.properties.__delitem__(key)
def keys(self): def keys(self):
return self.properties.keys() return self.properties.keys()
@ -132,6 +139,11 @@ class PropertyFilter(PropertyManager):
def __dict__(self): def __dict__(self):
return {k: v for k, v in self.pm.__dict__().items() if k in self.props} return {k: v for k, v in self.pm.__dict__().items() if k in self.props}
def __delitem__(self, key):
if key not in self.props:
raise KeyError(key)
return self.pm.__delitem__(key)
def keys(self): def keys(self):
return [k for k in self.pm.keys() if k in self.props] return [k for k in self.pm.keys() if k in self.props]
@ -226,5 +238,9 @@ class PropertyStack(PropertyManager):
def __dict__(self): def __dict__(self):
return {k: self.__getitem__(k) for k in self.keys()} return {k: self.__getitem__(k) for k in self.keys()}
def __delitem__(self, key):
for layer in self.layers:
layer["props"].__delitem__(key)
def keys(self): def keys(self):
return set([key for l in self.layers for key in l["props"].keys()]) return set([key for l in self.layers for key in l["props"].keys()])

View File

@ -30,7 +30,7 @@ class PskReporter(object):
sharedInstance = None sharedInstance = None
creationLock = threading.Lock() creationLock = threading.Lock()
interval = 300 interval = 300
supportedModes = ["FT8", "FT4", "JT9", "JT65"] supportedModes = ["FT8", "FT4", "JT9", "JT65", "JS8"]
@staticmethod @staticmethod
def getSharedInstance(): def getSharedInstance():

View File

@ -5,13 +5,14 @@ from owrx.bands import Bandplan
from csdr.csdr import dsp, output from csdr.csdr import dsp, output
from owrx.wsjt import WsjtParser from owrx.wsjt import WsjtParser
from owrx.aprs import AprsParser from owrx.aprs import AprsParser
from owrx.js8 import Js8Parser
from owrx.config import Config from owrx.config import Config
from owrx.source.resampler import Resampler from owrx.source.resampler import Resampler
from owrx.feature import FeatureDetector
from owrx.property import PropertyLayer from owrx.property import PropertyLayer
from js8py import Js8Frame
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from .schedule import ServiceScheduler from .schedule import ServiceScheduler
from functools import reduce from owrx.modes import Modes
import logging import logging
@ -50,28 +51,12 @@ class AprsServiceOutput(ServiceOutput):
return t == "packet_demod" return t == "packet_demod"
class ServiceDetector(object): class Js8ServiceOutput(ServiceOutput):
requirements = { def getParser(self):
"ft8": ["wsjt-x"], return Js8Parser(Js8Handler())
"ft4": ["wsjt-x"],
"jt65": ["wsjt-x"],
"jt9": ["wsjt-x"],
"wspr": ["wsjt-x"],
"packet": ["packet"],
}
@staticmethod def supports_type(self, t):
def getAvailableServices(): return t == "js8_demod"
# TODO this should be in a more central place (the frontend also needs this)
fd = FeatureDetector()
return [
name
for name, requirements in ServiceDetector.requirements.items()
if reduce(
lambda a, b: a and b, [fd.is_available(r) for r in requirements], True
)
]
class ServiceHandler(object): class ServiceHandler(object):
@ -109,7 +94,7 @@ class ServiceHandler(object):
def isSupported(self, mode): def isSupported(self, mode):
configured = Config.get()["services_decoders"] configured = Config.get()["services_decoders"]
available = ServiceDetector.getAvailableServices() available = [m.modulation for m in Modes.getAvailableServices()]
return mode in configured and mode in available return mode in configured and mode in available
def shutdown(self): def shutdown(self):
@ -258,6 +243,8 @@ class ServiceHandler(object):
# TODO selecting outputs will need some more intelligence here # TODO selecting outputs will need some more intelligence here
if mode == "packet": if mode == "packet":
output = AprsServiceOutput(frequency) output = AprsServiceOutput(frequency)
elif mode == "js8":
output = Js8ServiceOutput(frequency)
else: else:
output = WsjtServiceOutput(frequency) output = WsjtServiceOutput(frequency)
d = dsp(output) d = dsp(output)
@ -278,6 +265,7 @@ class ServiceHandler(object):
d.set_secondary_demodulator(mode) d.set_secondary_demodulator(mode)
d.set_audio_compression("none") d.set_audio_compression("none")
d.set_samp_rate(source.getProps()["samp_rate"]) d.set_samp_rate(source.getProps()["samp_rate"])
d.set_temporary_directory(Config.get()['temporary_directory'])
d.set_service() d.set_service()
d.start() d.start()
return d return d
@ -293,6 +281,11 @@ class AprsHandler(object):
pass pass
class Js8Handler(object):
def write_js8_message(self, frame: Js8Frame, freq: int):
pass
class Services(object): class Services(object):
handlers = [] handlers = []

View File

@ -1,5 +1,6 @@
from .direct import DirectSource from .direct import DirectSource
from owrx.command import Flag, Option from owrx.command import Option
import time
class HackrfSource(DirectSource): class HackrfSource(DirectSource):
@ -11,8 +12,12 @@ class HackrfSource(DirectSource):
"rf_gain": Option("-g"), "rf_gain": Option("-g"),
"lna_gain": Option("-l"), "lna_gain": Option("-l"),
"rf_amp": Option("-a"), "rf_amp": Option("-a"),
"ppm": Option("-C"),
} }
).setStatic("-r-") ).setStatic("-r-")
def getFormatConversion(self): def getFormatConversion(self):
return ["csdr convert_s8_f"] return ["csdr convert_s8_f"]
def sleepOnRestart(self):
time.sleep(1)

View File

@ -1,10 +1,4 @@
from .direct import DirectSource from .direct import DirectSource
from . import SdrSource
import subprocess
import threading
import os
import socket
import time
import logging import logging
@ -29,7 +23,7 @@ class Resampler(DirectSource):
def getCommand(self): def getCommand(self):
return [ return [
"nc -v 127.0.0.1 {nc_port}".format(nc_port=self.sdr.getPort()), "nc -v 127.0.0.1 {nc_port}".format(nc_port=self.sdr.getPort()),
"csdr shift_addition_cc {shift}".format(shift=self.shift), "csdr shift_addfast_cc {shift}".format(shift=self.shift),
"csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING".format( "csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING".format(
decimation=self.decimation, ddc_transition_bw=self.transition_bw decimation=self.decimation, ddc_transition_bw=self.transition_bw
), ),

View File

@ -9,6 +9,7 @@ class SdrplaySource(SoapyConnectorSource):
"bias_tee": "biasT_ctrl", "bias_tee": "biasT_ctrl",
"rf_notch": "rfnotch_ctrl", "rf_notch": "rfnotch_ctrl",
"dab_notch": "dabnotch_ctrl", "dab_notch": "dabnotch_ctrl",
"if_mode": "if_mode",
} }
) )
return mappings return mappings

View File

@ -199,9 +199,15 @@ class WebSocketConnection(object):
data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)]) data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)])
if opcode == OPCODE_TEXT_MESSAGE: if opcode == OPCODE_TEXT_MESSAGE:
message = data.decode("utf-8") message = data.decode("utf-8")
self.messageHandler.handleTextMessage(self, message) try:
self.messageHandler.handleTextMessage(self, message)
except Exception:
logger.exception("Exception in websocket handler handleTextMessage()")
elif opcode == OPCODE_BINARY_MESSAGE: elif opcode == OPCODE_BINARY_MESSAGE:
self.messageHandler.handleBinaryMessage(self, data) try:
self.messageHandler.handleBinaryMessage(self, data)
except Exception:
logger.exception("Exception in websocket handler handleBinaryMessage()")
elif opcode == OPCODE_PING: elif opcode == OPCODE_PING:
self.sendPong() self.sendPong()
elif opcode == OPCODE_PONG: elif opcode == OPCODE_PONG:

View File

@ -1,215 +1,19 @@
import threading from datetime import datetime, timezone
import wave
from datetime import datetime, timedelta, timezone
import subprocess
import os
from multiprocessing.connection import Pipe
from owrx.map import Map, LocatorLocation from owrx.map import Map, LocatorLocation
import re import re
from queue import Queue, Full from owrx.metrics import Metrics, CounterMetric
from owrx.config import Config
from owrx.metrics import Metrics, CounterMetric, DirectMetric
from owrx.pskreporter import PskReporter from owrx.pskreporter import PskReporter
from owrx.parser import Parser from owrx.parser import Parser
from owrx.audio import AudioChopperProfile
from abc import ABC, ABCMeta, abstractmethod from abc import ABC, ABCMeta, abstractmethod
from owrx.config import Config
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
class QueueJob(object): class WsjtProfile(AudioChopperProfile, metaclass=ABCMeta):
def __init__(self, decoder, file, freq):
self.decoder = decoder
self.file = file
self.freq = freq
def run(self):
self.decoder.decode(self)
class WsjtQueueWorker(threading.Thread):
def __init__(self, queue):
self.queue = queue
self.doRun = True
super().__init__(daemon=True)
def run(self) -> None:
while self.doRun:
job = self.queue.get()
try:
job.run()
except Exception:
logger.exception("failed to decode job")
self.queue.onError()
self.queue.task_done()
class WsjtQueue(Queue):
sharedInstance = None
creationLock = threading.Lock()
@staticmethod
def getSharedInstance():
with WsjtQueue.creationLock:
if WsjtQueue.sharedInstance is None:
pm = Config.get()
WsjtQueue.sharedInstance = WsjtQueue(maxsize=pm["wsjt_queue_length"], workers=pm["wsjt_queue_workers"])
return WsjtQueue.sharedInstance
def __init__(self, maxsize, workers):
super().__init__(maxsize)
metrics = Metrics.getSharedInstance()
metrics.addMetric("wsjt.queue.length", DirectMetric(self.qsize))
self.inCounter = CounterMetric()
metrics.addMetric("wsjt.queue.in", self.inCounter)
self.outCounter = CounterMetric()
metrics.addMetric("wsjt.queue.out", self.outCounter)
self.overflowCounter = CounterMetric()
metrics.addMetric("wsjt.queue.overflow", self.overflowCounter)
self.errorCounter = CounterMetric()
metrics.addMetric("wsjt.queue.error", self.errorCounter)
self.workers = [self.newWorker() for _ in range(0, workers)]
def put(self, item):
self.inCounter.inc()
try:
super(WsjtQueue, self).put(item, block=False)
except Full:
self.overflowCounter.inc()
raise
def get(self, **kwargs):
# super.get() is blocking, so it would mess up the stats to inc() first
out = super(WsjtQueue, self).get(**kwargs)
self.outCounter.inc()
return out
def newWorker(self):
worker = WsjtQueueWorker(self)
worker.start()
return worker
def onError(self):
self.errorCounter.inc()
class WsjtChopper(threading.Thread, metaclass=ABCMeta):
def __init__(self, dsp, source):
self.dsp = dsp
self.source = source
self.tmp_dir = Config.get()["temporary_directory"]
(self.wavefilename, self.wavefile) = self.getWaveFile()
self.switchingLock = threading.Lock()
self.timer = None
(self.outputReader, self.outputWriter) = Pipe()
self.doRun = True
super().__init__()
@abstractmethod
def getInterval(self):
pass
@abstractmethod
def getFileTimestampFormat(self):
pass
def getWaveFile(self):
filename = "{tmp_dir}/openwebrx-wsjtchopper-{id}-{timestamp}.wav".format(
tmp_dir=self.tmp_dir, id=id(self), timestamp=datetime.utcnow().strftime(self.getFileTimestampFormat())
)
wavefile = wave.open(filename, "wb")
wavefile.setnchannels(1)
wavefile.setsampwidth(2)
wavefile.setframerate(12000)
return filename, wavefile
def getNextDecodingTime(self):
t = datetime.utcnow()
zeroed = t.replace(minute=0, second=0, microsecond=0)
delta = t - zeroed
interval = self.getInterval()
seconds = (int(delta.total_seconds() / interval) + 1) * interval
t = zeroed + timedelta(seconds=seconds)
logger.debug("scheduling: {0}".format(t))
return t
def cancelTimer(self):
if self.timer:
self.timer.cancel()
def _scheduleNextSwitch(self):
if self.doRun:
delta = self.getNextDecodingTime() - datetime.utcnow()
self.timer = threading.Timer(delta.total_seconds(), self.switchFiles)
self.timer.start()
def switchFiles(self):
self.switchingLock.acquire()
file = self.wavefile
filename = self.wavefilename
(self.wavefilename, self.wavefile) = self.getWaveFile()
self.switchingLock.release()
file.close()
try:
WsjtQueue.getSharedInstance().put(QueueJob(self, filename, self.dsp.get_operating_freq()))
except Full:
logger.warning("wsjt decoding queue overflow; dropping one file")
os.unlink(filename)
self._scheduleNextSwitch()
@abstractmethod
def decoder_commandline(self, file):
pass
def decode(self, job: QueueJob):
logger.debug("processing file %s", job.file)
decoder = subprocess.Popen(
["nice", "-n", "10"] + self.decoder_commandline(job.file),
stdout=subprocess.PIPE,
cwd=self.tmp_dir,
close_fds=True,
)
for line in decoder.stdout:
self.outputWriter.send((job.freq, line))
try:
rc = decoder.wait(timeout=10)
if rc != 0:
logger.warning("decoder return code: %i", rc)
except subprocess.TimeoutExpired:
logger.warning("subprocess (pid=%i}) did not terminate correctly; sending kill signal.", decoder.pid)
decoder.kill()
os.unlink(job.file)
def run(self) -> None:
logger.debug("WSJT chopper starting up")
self._scheduleNextSwitch()
while self.doRun:
data = self.source.read(256)
if data is None or (isinstance(data, bytes) and len(data) == 0):
self.doRun = False
else:
self.switchingLock.acquire()
self.wavefile.writeframes(data)
self.switchingLock.release()
logger.debug("WSJT chopper shutting down")
self.outputReader.close()
self.outputWriter.close()
self.cancelTimer()
try:
os.unlink(self.wavefilename)
except Exception:
logger.exception("error removing undecoded file")
def read(self):
try:
return self.outputReader.recv()
except EOFError:
return None
def decoding_depth(self, mode): def decoding_depth(self, mode):
pm = Config.get() pm = Config.get()
# mode-specific setting? # mode-specific setting?
@ -222,7 +26,7 @@ class WsjtChopper(threading.Thread, metaclass=ABCMeta):
return 3 return 3
class Ft8Chopper(WsjtChopper): class Ft8Profile(WsjtProfile):
def getInterval(self): def getInterval(self):
return 15 return 15
@ -233,7 +37,7 @@ class Ft8Chopper(WsjtChopper):
return ["jt9", "--ft8", "-d", str(self.decoding_depth("ft8")), file] return ["jt9", "--ft8", "-d", str(self.decoding_depth("ft8")), file]
class WsprChopper(WsjtChopper): class WsprProfile(WsjtProfile):
def getInterval(self): def getInterval(self):
return 120 return 120
@ -248,7 +52,7 @@ class WsprChopper(WsjtChopper):
return cmd return cmd
class Jt65Chopper(WsjtChopper): class Jt65Profile(WsjtProfile):
def getInterval(self): def getInterval(self):
return 60 return 60
@ -259,7 +63,7 @@ class Jt65Chopper(WsjtChopper):
return ["jt9", "--jt65", "-d", str(self.decoding_depth("jt65")), file] return ["jt9", "--jt65", "-d", str(self.decoding_depth("jt65")), file]
class Jt9Chopper(WsjtChopper): class Jt9Profile(WsjtProfile):
def getInterval(self): def getInterval(self):
return 60 return 60
@ -270,7 +74,7 @@ class Jt9Chopper(WsjtChopper):
return ["jt9", "--jt9", "-d", str(self.decoding_depth("jt9")), file] return ["jt9", "--jt9", "-d", str(self.decoding_depth("jt9")), file]
class Ft4Chopper(WsjtChopper): class Ft4Profile(WsjtProfile):
def getInterval(self): def getInterval(self):
return 7.5 return 7.5
@ -284,34 +88,35 @@ class Ft4Chopper(WsjtChopper):
class WsjtParser(Parser): class WsjtParser(Parser):
modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4"} modes = {"~": "FT8", "#": "JT65", "@": "JT9", "+": "FT4"}
def parse(self, data): def parse(self, messages):
try: for data in messages:
freq, raw_msg = data try:
self.setDialFrequency(freq) freq, raw_msg = data
msg = raw_msg.decode().rstrip() self.setDialFrequency(freq)
# known debug messages we know to skip msg = raw_msg.decode().rstrip()
if msg.startswith("<DecodeFinished>"): # known debug messages we know to skip
return if msg.startswith("<DecodeFinished>"):
if msg.startswith(" EOF on input file"): return
return if msg.startswith(" EOF on input file"):
return
modes = list(WsjtParser.modes.keys()) modes = list(WsjtParser.modes.keys())
if msg[21] in modes or msg[19] in modes: if msg[21] in modes or msg[19] in modes:
decoder = Jt9Decoder() decoder = Jt9Decoder()
else: else:
decoder = WsprDecoder() decoder = WsprDecoder()
out = decoder.parse(msg, freq) out = decoder.parse(msg, freq)
if "mode" in out: if "mode" in out:
self.pushDecode(out["mode"]) self.pushDecode(out["mode"])
if "callsign" in out and "locator" in out: if "callsign" in out and "locator" in out:
Map.getSharedInstance().updateLocation( Map.getSharedInstance().updateLocation(
out["callsign"], LocatorLocation(out["locator"]), out["mode"], self.band out["callsign"], LocatorLocation(out["locator"]), out["mode"], self.band
) )
PskReporter.getSharedInstance().spot(out) PskReporter.getSharedInstance().spot(out)
self.handler.write_wsjt_message(out) self.handler.write_wsjt_message(out)
except ValueError: except ValueError:
logger.exception("error while parsing wsjt message") logger.exception("error while parsing wsjt message")
def pushDecode(self, mode): def pushDecode(self, mode):
metrics = Metrics.getSharedInstance() metrics = Metrics.getSharedInstance()

View File

@ -14,8 +14,7 @@ setup(
packages=find_namespace_packages(include=["owrx", "owrx.source", "owrx.service", "owrx.controllers", "owrx.property", "owrx.form", "csdr", "htdocs"]), packages=find_namespace_packages(include=["owrx", "owrx.source", "owrx.service", "owrx.controllers", "owrx.property", "owrx.form", "csdr", "htdocs"]),
package_data={"htdocs": [f[len("htdocs/") :] for f in glob("htdocs/**/*", recursive=True)]}, package_data={"htdocs": [f[len("htdocs/") :] for f in glob("htdocs/**/*", recursive=True)]},
entry_points={"console_scripts": ["openwebrx=owrx.__main__:main"]}, entry_points={"console_scripts": ["openwebrx=owrx.__main__:main"]},
# use the github page for now url="https://www.openwebrx.de/",
url="https://github.com/jketterl/openwebrx",
author="Jakob Ketterl", author="Jakob Ketterl",
author_email="jakob.ketterl@gmx.de", author_email="jakob.ketterl@gmx.de",
maintainer="Jakob Ketterl", maintainer="Jakob Ketterl",