2018-09-25 12:56:47 +00:00
|
|
|
"""
|
|
|
|
OpenWebRX csdr plugin: do the signal processing with csdr
|
|
|
|
|
|
|
|
This file is part of OpenWebRX,
|
|
|
|
an open-source SDR receiver software with a web UI.
|
|
|
|
Copyright (c) 2013-2015 by Andras Retzler <randras@sdr.hu>
|
2021-01-22 17:10:51 +00:00
|
|
|
Copyright (c) 2019-2021 by Jakob Ketterl <dd5jfk@darc.de>
|
2018-09-25 12:56:47 +00:00
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU Affero General Public License as
|
|
|
|
published by the Free Software Foundation, either version 3 of the
|
|
|
|
License, or (at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
import subprocess
|
|
|
|
import os
|
|
|
|
import signal
|
2019-05-07 16:19:53 +00:00
|
|
|
import threading
|
2019-11-23 15:56:29 +00:00
|
|
|
import math
|
2019-05-14 21:30:03 +00:00
|
|
|
from functools import partial
|
2019-08-11 14:37:30 +00:00
|
|
|
|
2021-04-09 16:16:25 +00:00
|
|
|
from csdr.output import Output
|
|
|
|
|
2021-04-07 14:20:10 +00:00
|
|
|
from owrx.kiss import KissClient, DirewolfConfig, DirewolfConfigSubscriber
|
2021-04-11 12:40:28 +00:00
|
|
|
from owrx.audio.chopper import AudioChopper
|
2016-06-07 18:51:04 +00:00
|
|
|
|
2020-08-14 17:54:07 +00:00
|
|
|
from csdr.pipe import Pipe
|
2020-12-25 19:27:30 +00:00
|
|
|
|
2021-07-24 16:50:30 +00:00
|
|
|
from pycsdr.modules import Buffer
|
2021-07-24 22:05:48 +00:00
|
|
|
from pycsdr.types import Format
|
2021-07-19 17:04:14 +00:00
|
|
|
from csdr.chain.demodulator import DemodulatorChain
|
2021-07-25 20:44:53 +00:00
|
|
|
from csdr.chain.fm import NFm, WFm
|
2021-07-19 21:32:03 +00:00
|
|
|
from csdr.chain.am import Am
|
2021-07-19 22:57:43 +00:00
|
|
|
from csdr.chain.ssb import Ssb
|
2021-08-06 18:02:59 +00:00
|
|
|
from csdr.chain.digiham import Dstar, Nxdn, Dmr
|
2021-07-25 17:31:56 +00:00
|
|
|
from csdr.chain.clientaudio import ClientAudioChain
|
2021-07-19 17:04:14 +00:00
|
|
|
|
2019-05-10 20:07:26 +00:00
|
|
|
import logging
|
2019-07-21 17:40:28 +00:00
|
|
|
|
2019-05-10 20:07:26 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
2018-09-25 12:56:47 +00:00
|
|
|
|
2019-07-21 17:40:28 +00:00
|
|
|
|
2021-04-09 16:16:25 +00:00
|
|
|
class Dsp(DirewolfConfigSubscriber):
|
|
|
|
def __init__(self, output: Output):
|
2020-12-16 09:18:47 +00:00
|
|
|
self.pycsdr_enabled = True
|
2020-12-16 17:52:00 +00:00
|
|
|
self.pycsdr_chain = None
|
2021-07-25 17:31:56 +00:00
|
|
|
self.pycsdr_client_chain = None
|
2021-07-24 16:50:30 +00:00
|
|
|
self.pycsdr_reader = None
|
2021-07-24 22:05:48 +00:00
|
|
|
self.pycsdr_power_reader = None
|
2021-07-31 22:49:20 +00:00
|
|
|
self.pycsdr_meta_reader = None
|
2020-12-25 19:27:30 +00:00
|
|
|
self.buffer = None
|
2020-12-16 09:18:47 +00:00
|
|
|
|
2018-09-25 12:56:47 +00:00
|
|
|
self.samp_rate = 250000
|
2019-09-13 20:28:17 +00:00
|
|
|
self.output_rate = 11025
|
2020-08-08 19:29:25 +00:00
|
|
|
self.hd_output_rate = 44100
|
2018-09-25 12:56:47 +00:00
|
|
|
self.fft_fps = 5
|
2020-04-05 14:35:46 +00:00
|
|
|
self.center_freq = 0
|
2018-09-25 12:56:47 +00:00
|
|
|
self.offset_freq = 0
|
|
|
|
self.low_cut = -4000
|
|
|
|
self.high_cut = 4000
|
2019-07-21 17:40:28 +00:00
|
|
|
self.bpf_transition_bw = 320 # Hz, and this is a constant
|
|
|
|
self.ddc_transition_bw_rate = 0.15 # of the IF sample rate
|
2018-09-25 12:56:47 +00:00
|
|
|
self.running = False
|
|
|
|
self.secondary_processes_running = False
|
|
|
|
self.audio_compression = "none"
|
|
|
|
self.fft_compression = "none"
|
|
|
|
self.demodulator = "nfm"
|
|
|
|
self.name = "csdr"
|
|
|
|
self.base_bufsize = 512
|
2020-03-23 21:09:05 +00:00
|
|
|
self.decimation = None
|
|
|
|
self.last_decimation = None
|
2019-09-13 20:28:17 +00:00
|
|
|
self.nc_port = None
|
2019-11-23 15:56:29 +00:00
|
|
|
self.squelch_level = -150
|
2018-09-25 12:56:47 +00:00
|
|
|
self.fft_averages = 50
|
2020-10-04 19:46:58 +00:00
|
|
|
self.wfm_deemphasis_tau = 50e-6
|
2018-09-25 12:56:47 +00:00
|
|
|
self.iqtee = False
|
|
|
|
self.iqtee2 = False
|
|
|
|
self.secondary_demodulator = None
|
|
|
|
self.secondary_fft_size = 1024
|
|
|
|
self.secondary_process_fft = None
|
|
|
|
self.secondary_process_demod = None
|
2020-02-27 23:20:37 +00:00
|
|
|
self.pipe_names = {
|
|
|
|
"bpf_pipe": Pipe.WRITE,
|
|
|
|
"shift_pipe": Pipe.WRITE,
|
|
|
|
"squelch_pipe": Pipe.WRITE,
|
|
|
|
"smeter_pipe": Pipe.READ,
|
|
|
|
"meta_pipe": Pipe.READ,
|
|
|
|
"iqtee_pipe": Pipe.NONE,
|
|
|
|
"iqtee2_pipe": Pipe.NONE,
|
|
|
|
"dmr_control_pipe": Pipe.WRITE,
|
|
|
|
}
|
2020-02-25 19:55:42 +00:00
|
|
|
self.pipes = {}
|
2020-02-27 23:20:37 +00:00
|
|
|
self.secondary_pipe_names = {"secondary_shift_pipe": Pipe.WRITE}
|
2018-09-25 12:56:47 +00:00
|
|
|
self.secondary_offset_freq = 1000
|
2021-05-29 16:50:17 +00:00
|
|
|
self.codecserver = None
|
2019-05-08 14:31:52 +00:00
|
|
|
self.modification_lock = threading.Lock()
|
2019-05-14 21:30:03 +00:00
|
|
|
self.output = output
|
2020-02-27 18:48:22 +00:00
|
|
|
|
|
|
|
self.temporary_directory = None
|
|
|
|
self.pipe_base_path = None
|
|
|
|
self.set_temporary_directory("/tmp")
|
|
|
|
|
2019-09-13 20:28:17 +00:00
|
|
|
self.is_service = False
|
2019-08-17 20:38:09 +00:00
|
|
|
self.direwolf_config = None
|
2021-04-07 14:20:10 +00:00
|
|
|
self.direwolf_config_path = None
|
2020-02-27 17:50:53 +00:00
|
|
|
self.process = None
|
2018-09-25 12:56:47 +00:00
|
|
|
|
2020-12-25 19:27:30 +00:00
|
|
|
def setBuffer(self, buffer):
|
|
|
|
self.buffer = buffer
|
2020-12-19 15:28:18 +00:00
|
|
|
if self.pycsdr_chain is not None:
|
2020-12-25 19:27:30 +00:00
|
|
|
self.pycsdr_chain.setInput(buffer)
|
2020-12-19 15:28:18 +00:00
|
|
|
|
2019-09-13 20:28:17 +00:00
|
|
|
def set_service(self, flag=True):
|
|
|
|
self.is_service = flag
|
|
|
|
|
2019-07-13 15:16:38 +00:00
|
|
|
def set_temporary_directory(self, what):
|
|
|
|
self.temporary_directory = what
|
2020-08-13 21:51:11 +00:00
|
|
|
self.pipe_base_path = "{tmp_dir}/openwebrx_pipe_".format(tmp_dir=self.temporary_directory)
|
2018-09-25 12:56:47 +00:00
|
|
|
|
2019-07-21 17:40:28 +00:00
|
|
|
def chain(self, which):
|
2021-07-19 21:32:03 +00:00
|
|
|
if self.pycsdr_enabled:
|
|
|
|
if which == "nfm":
|
2021-07-25 20:44:53 +00:00
|
|
|
self.pycsdr_chain = DemodulatorChain(self.samp_rate, self.get_audio_rate(), 0.0, NFm(self.get_audio_rate()))
|
2021-07-19 21:32:03 +00:00
|
|
|
return self.pycsdr_chain
|
|
|
|
elif which == "am":
|
|
|
|
self.pycsdr_chain = DemodulatorChain(self.samp_rate, self.get_audio_rate(), 0.0, Am())
|
|
|
|
return self.pycsdr_chain
|
2021-07-19 22:57:43 +00:00
|
|
|
elif which == "ssb":
|
|
|
|
self.pycsdr_chain = DemodulatorChain(self.samp_rate, self.get_audio_rate(), 0.0, Ssb())
|
|
|
|
return self.pycsdr_chain
|
2021-07-25 20:44:53 +00:00
|
|
|
elif which == "wfm":
|
|
|
|
self.pycsdr_chain = DemodulatorChain(self.samp_rate, 200000, 0.0, WFm(self.get_audio_rate(), self.wfm_deemphasis_tau))
|
|
|
|
return self.pycsdr_chain
|
2021-07-29 22:06:21 +00:00
|
|
|
elif which == "dstar":
|
|
|
|
self.pycsdr_chain = DemodulatorChain(self.samp_rate, 48000, 0.0, Dstar(self.codecserver))
|
|
|
|
return self.pycsdr_chain
|
2021-07-30 22:10:10 +00:00
|
|
|
elif which == "nxdn":
|
|
|
|
self.pycsdr_chain = DemodulatorChain(self.samp_rate, 48000, 0.0, Nxdn(self.codecserver))
|
|
|
|
return self.pycsdr_chain
|
2021-08-06 18:02:59 +00:00
|
|
|
elif which == "dmr":
|
|
|
|
self.pycsdr_chain = DemodulatorChain(self.samp_rate, 48000, 0.0, Dmr(self.codecserver))
|
|
|
|
return self.pycsdr_chain
|
2021-07-19 17:04:14 +00:00
|
|
|
|
2019-06-30 14:24:56 +00:00
|
|
|
chain = ["nc -v 127.0.0.1 {nc_port}"]
|
2020-05-24 01:04:20 +00:00
|
|
|
chain += ["csdr shift_addfast_cc --fifo {shift_pipe}"]
|
2019-09-10 22:27:49 +00:00
|
|
|
if self.decimation > 1:
|
|
|
|
chain += ["csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING"]
|
|
|
|
chain += ["csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING"]
|
2019-08-11 09:37:45 +00:00
|
|
|
if self.output.supports_type("smeter"):
|
2019-08-04 12:55:56 +00:00
|
|
|
chain += [
|
2019-08-11 09:37:45 +00:00
|
|
|
"csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 {smeter_report_every}"
|
2019-08-04 12:55:56 +00:00
|
|
|
]
|
2018-09-25 12:56:47 +00:00
|
|
|
if self.secondary_demodulator:
|
2019-08-11 09:37:45 +00:00
|
|
|
if self.output.supports_type("secondary_fft"):
|
2019-08-04 12:55:56 +00:00
|
|
|
chain += ["csdr tee {iqtee_pipe}"]
|
|
|
|
chain += ["csdr tee {iqtee2_pipe}"]
|
|
|
|
# early exit if we don't want audio
|
2019-08-11 09:37:45 +00:00
|
|
|
if not self.output.supports_type("audio"):
|
2019-08-04 12:55:56 +00:00
|
|
|
return chain
|
2019-05-16 19:26:31 +00:00
|
|
|
# safe some cpu cycles... no need to decimate if decimation factor is 1
|
2020-08-08 20:51:03 +00:00
|
|
|
last_decimation_block = []
|
|
|
|
if self.last_decimation >= 2.0:
|
|
|
|
# activate prefilter if signal has been oversampled, e.g. WFM
|
|
|
|
last_decimation_block = ["csdr fractional_decimator_ff {last_decimation} 12 --prefilter"]
|
2020-08-27 20:35:12 +00:00
|
|
|
elif self.last_decimation != 1.0:
|
2020-08-08 20:51:03 +00:00
|
|
|
last_decimation_block = ["csdr fractional_decimator_ff {last_decimation}"]
|
2019-05-15 21:08:55 +00:00
|
|
|
if which == "nfm":
|
2019-07-21 17:40:28 +00:00
|
|
|
chain += ["csdr fmdemod_quadri_cf", "csdr limit_ff"]
|
2019-05-16 19:26:31 +00:00
|
|
|
chain += last_decimation_block
|
2020-08-27 17:28:20 +00:00
|
|
|
chain += [
|
|
|
|
"csdr deemphasis_nfm_ff {audio_rate}",
|
|
|
|
"csdr agc_ff --profile slow --max 3",
|
|
|
|
]
|
2019-08-11 11:52:19 +00:00
|
|
|
if self.get_audio_rate() != self.get_output_rate():
|
|
|
|
chain += [
|
|
|
|
"sox -t raw -r {audio_rate} -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - "
|
|
|
|
]
|
|
|
|
else:
|
|
|
|
chain += ["csdr convert_f_s16"]
|
2020-08-08 19:29:25 +00:00
|
|
|
elif which == "wfm":
|
2020-08-08 20:51:03 +00:00
|
|
|
chain += [
|
|
|
|
"csdr fmdemod_quadri_cf",
|
|
|
|
"csdr limit_ff",
|
|
|
|
]
|
2020-08-08 19:29:25 +00:00
|
|
|
chain += last_decimation_block
|
2021-01-20 16:01:46 +00:00
|
|
|
chain += ["csdr deemphasis_wfm_ff {audio_rate} {wfm_deemphasis_tau}", "csdr convert_f_s16"]
|
2019-05-16 19:26:31 +00:00
|
|
|
elif self.isDigitalVoice(which):
|
2020-12-27 18:49:59 +00:00
|
|
|
chain += ["csdr fmdemod_quadri_cf"]
|
2019-05-16 19:26:31 +00:00
|
|
|
chain += last_decimation_block
|
2021-05-27 19:35:55 +00:00
|
|
|
chain += ["dc_block"]
|
2020-11-23 00:00:25 +00:00
|
|
|
# m17
|
2021-05-27 19:35:55 +00:00
|
|
|
if which == "m17":
|
2020-11-23 00:00:25 +00:00
|
|
|
chain += [
|
|
|
|
"csdr limit_ff",
|
|
|
|
"csdr convert_f_s16",
|
|
|
|
"m17-demod",
|
|
|
|
]
|
2019-05-24 23:46:16 +00:00
|
|
|
else:
|
2021-06-15 20:50:30 +00:00
|
|
|
# digiham modes
|
2021-06-08 11:36:08 +00:00
|
|
|
if which == "dstar":
|
2019-06-30 14:24:56 +00:00
|
|
|
chain += [
|
2021-06-08 11:36:08 +00:00
|
|
|
"fsk_demodulator -s 10",
|
|
|
|
"dstar_decoder --fifo {meta_pipe}",
|
|
|
|
"mbe_synthesizer -d {codecserver_arg}",
|
2019-06-30 14:24:56 +00:00
|
|
|
]
|
2021-06-08 11:36:08 +00:00
|
|
|
elif which == "nxdn":
|
2021-06-15 20:50:30 +00:00
|
|
|
chain += [
|
|
|
|
"rrc_filter --narrow",
|
|
|
|
"gfsk_demodulator --samples 20",
|
|
|
|
"nxdn_decoder --fifo {meta_pipe}",
|
|
|
|
"mbe_synthesizer {codecserver_arg}",
|
|
|
|
]
|
2021-05-27 19:35:55 +00:00
|
|
|
else:
|
|
|
|
chain += ["rrc_filter", "gfsk_demodulator"]
|
|
|
|
if which == "dmr":
|
|
|
|
chain += [
|
|
|
|
"dmr_decoder --fifo {meta_pipe} --control-fifo {dmr_control_pipe}",
|
2021-05-29 16:50:17 +00:00
|
|
|
"mbe_synthesizer {codecserver_arg}",
|
2021-05-27 19:35:55 +00:00
|
|
|
]
|
|
|
|
elif which == "ysf":
|
2021-05-29 16:50:17 +00:00
|
|
|
chain += ["ysf_decoder --fifo {meta_pipe}", "mbe_synthesizer -y {codecserver_arg}"]
|
2021-05-27 19:35:55 +00:00
|
|
|
chain += ["digitalvoice_filter"]
|
|
|
|
chain += [
|
|
|
|
"CSDR_FIXED_BUFSIZE=32 csdr agc_s16 --max 30 --initial 3",
|
|
|
|
"sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - ",
|
|
|
|
]
|
2019-05-15 21:08:55 +00:00
|
|
|
elif which == "am":
|
2019-07-21 17:40:28 +00:00
|
|
|
chain += ["csdr amdemod_cf", "csdr fastdcblock_ff"]
|
2019-05-16 19:26:31 +00:00
|
|
|
chain += last_decimation_block
|
2020-01-24 10:42:20 +00:00
|
|
|
chain += [
|
2020-08-26 17:45:21 +00:00
|
|
|
"csdr agc_ff --profile slow --initial 200",
|
2020-01-24 10:42:20 +00:00
|
|
|
"csdr convert_f_s16",
|
|
|
|
]
|
2020-08-08 19:29:25 +00:00
|
|
|
elif self.isFreeDV(which):
|
2020-07-27 22:28:20 +00:00
|
|
|
chain += ["csdr realpart_cf"]
|
|
|
|
chain += last_decimation_block
|
|
|
|
chain += [
|
|
|
|
"csdr agc_ff",
|
|
|
|
"csdr convert_f_s16",
|
|
|
|
"freedv_rx 1600 - -",
|
2020-08-29 19:32:21 +00:00
|
|
|
"csdr agc_s16 --max 30 --initial 3",
|
2020-07-27 22:28:20 +00:00
|
|
|
"sox -t raw -r 8000 -e signed-integer -b 16 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - ",
|
|
|
|
]
|
2020-09-04 16:09:02 +00:00
|
|
|
elif self.isDrm(which):
|
2020-09-04 20:02:23 +00:00
|
|
|
if self.last_decimation != 1.0:
|
|
|
|
# we are still dealing with complex samples here, so the regular last_decimation_block doesn't fit
|
|
|
|
chain += ["csdr fractional_decimator_cc {last_decimation}"]
|
2020-09-04 17:14:16 +00:00
|
|
|
chain += [
|
|
|
|
"csdr convert_f_s16",
|
|
|
|
"dream -c 6 --sigsrate 48000 --audsrate 48000 -I - -O -",
|
|
|
|
"sox -t raw -r 48000 -e signed-integer -b 16 -c 2 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - ",
|
|
|
|
]
|
2019-05-15 21:08:55 +00:00
|
|
|
elif which == "ssb":
|
2019-06-30 14:24:56 +00:00
|
|
|
chain += ["csdr realpart_cf"]
|
2019-05-16 19:26:31 +00:00
|
|
|
chain += last_decimation_block
|
2020-08-28 20:05:00 +00:00
|
|
|
chain += ["csdr agc_ff"]
|
2019-07-10 20:31:06 +00:00
|
|
|
# fixed sample rate necessary for the wsjt-x tools. fix with sox...
|
2019-08-11 11:52:19 +00:00
|
|
|
if self.get_audio_rate() != self.get_output_rate():
|
2019-07-10 20:31:06 +00:00
|
|
|
chain += [
|
|
|
|
"sox -t raw -r {audio_rate} -e floating-point -b 32 -c 1 --buffer 32 - -t raw -r {output_rate} -e signed-integer -b 16 -c 1 - "
|
|
|
|
]
|
|
|
|
else:
|
|
|
|
chain += ["csdr convert_f_s16"]
|
2019-05-15 21:08:55 +00:00
|
|
|
|
2019-07-21 17:40:28 +00:00
|
|
|
if self.audio_compression == "adpcm":
|
2021-07-25 18:06:14 +00:00
|
|
|
chain += ["csdr++ adpcm -e --sync"]
|
2019-05-15 21:08:55 +00:00
|
|
|
return chain
|
2018-09-25 12:56:47 +00:00
|
|
|
|
|
|
|
def secondary_chain(self, which):
|
2019-11-22 14:00:36 +00:00
|
|
|
chain = ["cat {input_pipe}"]
|
2018-09-25 12:56:47 +00:00
|
|
|
if which == "fft":
|
2019-11-22 14:00:36 +00:00
|
|
|
chain += [
|
2020-09-17 20:21:49 +00:00
|
|
|
"csdr fft_cc {secondary_fft_input_size} {secondary_fft_block_size}",
|
2020-09-17 20:43:39 +00:00
|
|
|
"csdr logpower_cf -70"
|
|
|
|
if self.fft_averages == 0
|
|
|
|
else "csdr logaveragepower_cf -70 {secondary_fft_size} {fft_averages}",
|
2020-09-17 20:21:49 +00:00
|
|
|
"csdr fft_exchange_sides_ff {secondary_fft_input_size}",
|
2019-11-22 14:00:36 +00:00
|
|
|
]
|
|
|
|
if self.fft_compression == "adpcm":
|
|
|
|
chain += ["csdr compress_fft_adpcm_f_u8 {secondary_fft_size}"]
|
|
|
|
return chain
|
2020-01-13 19:10:14 +00:00
|
|
|
elif which == "bpsk31" or which == "bpsk63":
|
2019-11-22 14:00:36 +00:00
|
|
|
return chain + [
|
2020-05-24 01:04:20 +00:00
|
|
|
"csdr shift_addfast_cc --fifo {secondary_shift_pipe}",
|
2019-11-22 14:00:36 +00:00
|
|
|
"csdr bandpass_fir_fft_cc -{secondary_bpf_cutoff} {secondary_bpf_cutoff} {secondary_bpf_cutoff}",
|
|
|
|
"csdr simple_agc_cc 0.001 0.5",
|
|
|
|
"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 psk31_varicode_decoder_u8_u8",
|
|
|
|
]
|
2020-04-25 17:05:24 +00:00
|
|
|
elif self.isWsjtMode(which) or self.isJs8(which):
|
2019-11-22 14:00:36 +00:00
|
|
|
chain += ["csdr realpart_cf"]
|
2019-07-21 17:40:28 +00:00
|
|
|
if self.last_decimation != 1.0:
|
2019-11-22 14:00:36 +00:00
|
|
|
chain += ["csdr fractional_decimator_ff {last_decimation}"]
|
2020-10-25 13:41:53 +00:00
|
|
|
return chain + ["csdr agc_ff", "csdr convert_f_s16"]
|
2019-08-11 11:52:19 +00:00
|
|
|
elif which == "packet":
|
2019-11-22 14:00:36 +00:00
|
|
|
chain += ["csdr fmdemod_quadri_cf"]
|
2019-08-11 11:52:19 +00:00
|
|
|
if self.last_decimation != 1.0:
|
2019-11-22 14:00:36 +00:00
|
|
|
chain += ["csdr fractional_decimator_ff {last_decimation}"]
|
2020-02-17 11:06:13 +00:00
|
|
|
return chain + ["csdr convert_f_s16", "direwolf -c {direwolf_config} -r {audio_rate} -t 0 -q d -q h 1>&2"]
|
2020-01-06 21:08:17 +00:00
|
|
|
elif which == "pocsag":
|
|
|
|
chain += ["csdr fmdemod_quadri_cf"]
|
|
|
|
if self.last_decimation != 1.0:
|
|
|
|
chain += ["csdr fractional_decimator_ff {last_decimation}"]
|
2020-01-07 06:30:19 +00:00
|
|
|
return chain + ["fsk_demodulator -i", "pocsag_decoder"]
|
2018-09-25 12:56:47 +00:00
|
|
|
|
|
|
|
def set_secondary_demodulator(self, what):
|
2019-05-18 19:38:15 +00:00
|
|
|
if self.get_secondary_demodulator() == what:
|
|
|
|
return
|
2018-09-25 12:56:47 +00:00
|
|
|
self.secondary_demodulator = what
|
2019-07-12 17:28:40 +00:00
|
|
|
self.calculate_decimation()
|
2019-05-18 19:38:15 +00:00
|
|
|
self.restart()
|
2018-09-25 12:56:47 +00:00
|
|
|
|
|
|
|
def secondary_fft_block_size(self):
|
2020-09-17 20:57:40 +00:00
|
|
|
base = (self.samp_rate / self.decimation) / (self.fft_fps * 2)
|
2020-09-17 20:43:39 +00:00
|
|
|
if self.fft_averages == 0:
|
2020-09-17 20:57:40 +00:00
|
|
|
return base
|
|
|
|
return base / self.fft_averages
|
2018-09-25 12:56:47 +00:00
|
|
|
|
|
|
|
def secondary_decimation(self):
|
2019-07-21 17:40:28 +00:00
|
|
|
return 1 # currently unused
|
2018-09-25 12:56:47 +00:00
|
|
|
|
|
|
|
def secondary_bpf_cutoff(self):
|
|
|
|
if self.secondary_demodulator == "bpsk31":
|
2019-07-21 17:40:28 +00:00
|
|
|
return 31.25 / self.if_samp_rate()
|
2020-01-13 19:10:14 +00:00
|
|
|
elif self.secondary_demodulator == "bpsk63":
|
|
|
|
return 62.5 / self.if_samp_rate()
|
2018-09-25 12:56:47 +00:00
|
|
|
return 0
|
|
|
|
|
|
|
|
def secondary_bpf_transition_bw(self):
|
|
|
|
if self.secondary_demodulator == "bpsk31":
|
2019-05-05 20:09:48 +00:00
|
|
|
return 31.25 / self.if_samp_rate()
|
2020-01-13 19:10:14 +00:00
|
|
|
elif self.secondary_demodulator == "bpsk63":
|
|
|
|
return 62.5 / self.if_samp_rate()
|
2018-09-25 12:56:47 +00:00
|
|
|
return 0
|
|
|
|
|
|
|
|
def secondary_samples_per_bits(self):
|
|
|
|
if self.secondary_demodulator == "bpsk31":
|
2019-07-21 17:40:28 +00:00
|
|
|
return int(round(self.if_samp_rate() / 31.25)) & ~3
|
2020-01-13 19:10:14 +00:00
|
|
|
elif self.secondary_demodulator == "bpsk63":
|
|
|
|
return int(round(self.if_samp_rate() / 62.5)) & ~3
|
2018-09-25 12:56:47 +00:00
|
|
|
return 0
|
|
|
|
|
|
|
|
def secondary_bw(self):
|
|
|
|
if self.secondary_demodulator == "bpsk31":
|
|
|
|
return 31.25
|
2020-01-13 19:10:14 +00:00
|
|
|
elif self.secondary_demodulator == "bpsk63":
|
|
|
|
return 62.5
|
2018-09-25 12:56:47 +00:00
|
|
|
|
|
|
|
def start_secondary_demodulator(self):
|
2019-07-21 17:40:28 +00:00
|
|
|
if not self.secondary_demodulator:
|
|
|
|
return
|
2019-07-28 09:40:58 +00:00
|
|
|
logger.debug("starting secondary demodulator from IF input sampled at %d" % self.if_samp_rate())
|
2019-11-22 14:00:36 +00:00
|
|
|
secondary_command_demod = " | ".join(self.secondary_chain(self.secondary_demodulator))
|
2019-08-04 12:55:56 +00:00
|
|
|
self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod)
|
2019-08-15 23:27:03 +00:00
|
|
|
self.try_create_configs(secondary_command_demod)
|
2018-09-25 12:56:47 +00:00
|
|
|
|
2019-07-21 17:40:28 +00:00
|
|
|
secondary_command_demod = secondary_command_demod.format(
|
2020-02-25 19:55:42 +00:00
|
|
|
input_pipe=self.pipes["iqtee2_pipe"],
|
|
|
|
secondary_shift_pipe=self.pipes["secondary_shift_pipe"],
|
2019-05-10 19:29:05 +00:00
|
|
|
secondary_decimation=self.secondary_decimation(),
|
|
|
|
secondary_samples_per_bits=self.secondary_samples_per_bits(),
|
|
|
|
secondary_bpf_cutoff=self.secondary_bpf_cutoff(),
|
|
|
|
secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(),
|
2019-07-06 16:21:43 +00:00
|
|
|
if_samp_rate=self.if_samp_rate(),
|
2019-07-21 17:40:28 +00:00
|
|
|
last_decimation=self.last_decimation,
|
2019-08-11 11:52:19 +00:00
|
|
|
audio_rate=self.get_audio_rate(),
|
2021-04-07 14:20:10 +00:00
|
|
|
direwolf_config=self.direwolf_config_path,
|
2019-07-21 17:40:28 +00:00
|
|
|
)
|
2018-09-25 12:56:47 +00:00
|
|
|
|
2019-07-28 09:40:58 +00:00
|
|
|
logger.debug("secondary command (demod) = %s", secondary_command_demod)
|
2019-08-11 09:37:45 +00:00
|
|
|
if self.output.supports_type("secondary_fft"):
|
2019-11-22 14:00:36 +00:00
|
|
|
secondary_command_fft = " | ".join(self.secondary_chain("fft"))
|
2019-08-04 12:55:56 +00:00
|
|
|
secondary_command_fft = secondary_command_fft.format(
|
2020-02-25 19:55:42 +00:00
|
|
|
input_pipe=self.pipes["iqtee_pipe"],
|
2019-08-04 12:55:56 +00:00
|
|
|
secondary_fft_input_size=self.secondary_fft_size,
|
|
|
|
secondary_fft_size=self.secondary_fft_size,
|
|
|
|
secondary_fft_block_size=self.secondary_fft_block_size(),
|
2020-09-17 20:43:39 +00:00
|
|
|
fft_averages=self.fft_averages,
|
2018-09-25 12:56:47 +00:00
|
|
|
)
|
2019-08-04 12:55:56 +00:00
|
|
|
logger.debug("secondary command (fft) = %s", secondary_command_fft)
|
2018-09-25 12:56:47 +00:00
|
|
|
|
2019-08-04 12:55:56 +00:00
|
|
|
self.secondary_process_fft = subprocess.Popen(
|
2021-02-11 21:51:50 +00:00
|
|
|
secondary_command_fft, stdout=subprocess.PIPE, shell=True, start_new_session=True
|
2019-08-04 12:55:56 +00:00
|
|
|
)
|
|
|
|
self.output.send_output(
|
|
|
|
"secondary_fft",
|
|
|
|
partial(self.secondary_process_fft.stdout.read, int(self.get_secondary_fft_bytes_to_read())),
|
|
|
|
)
|
2018-09-25 12:56:47 +00:00
|
|
|
|
2019-08-17 17:59:58 +00:00
|
|
|
# direwolf does not provide any meaningful data on stdout
|
|
|
|
# more specifically, it doesn't provide any data. if however, for any strange reason, it would start to do so,
|
|
|
|
# it would block if not read. by piping it to devnull, we avoid a potential pitfall here.
|
|
|
|
secondary_output = subprocess.DEVNULL if self.isPacket() else subprocess.PIPE
|
2019-07-21 17:40:28 +00:00
|
|
|
self.secondary_process_demod = subprocess.Popen(
|
2021-02-11 22:07:45 +00:00
|
|
|
secondary_command_demod, stdout=secondary_output, shell=True, start_new_session=True
|
2019-07-28 09:40:58 +00:00
|
|
|
)
|
2018-09-25 12:56:47 +00:00
|
|
|
self.secondary_processes_running = True
|
2019-05-14 21:30:03 +00:00
|
|
|
|
2021-04-09 16:16:25 +00:00
|
|
|
if self.isWsjtMode() or self.isJs8():
|
2021-04-11 12:40:28 +00:00
|
|
|
chopper = AudioChopper(self, self.get_secondary_demodulator())
|
|
|
|
chopper.send_output("audio", self.secondary_process_demod.stdout.read)
|
2021-04-09 20:40:30 +00:00
|
|
|
output_type = "js8_demod" if self.isJs8() else "wsjt_demod"
|
2021-04-11 12:40:28 +00:00
|
|
|
self.output.send_output(output_type, chopper.read)
|
2019-08-17 17:59:58 +00:00
|
|
|
elif self.isPacket():
|
|
|
|
# we best get the ax25 packets from the kiss socket
|
2021-04-07 14:20:10 +00:00
|
|
|
kiss = KissClient(self.direwolf_config.getPort())
|
2019-08-11 14:37:30 +00:00
|
|
|
self.output.send_output("packet_demod", kiss.read)
|
2020-01-09 14:12:51 +00:00
|
|
|
elif self.isPocsag():
|
|
|
|
self.output.send_output("pocsag_demod", self.secondary_process_demod.stdout.readline)
|
2019-08-17 17:59:58 +00:00
|
|
|
else:
|
|
|
|
self.output.send_output("secondary_demod", partial(self.secondary_process_demod.stdout.read, 1))
|
2019-08-11 14:37:30 +00:00
|
|
|
|
2019-07-21 17:40:28 +00:00
|
|
|
# open control pipes for csdr and send initialization data
|
2020-02-25 19:55:42 +00:00
|
|
|
if self.has_pipe("secondary_shift_pipe"): # TODO digimodes
|
2019-07-21 17:40:28 +00:00
|
|
|
self.set_secondary_offset_freq(self.secondary_offset_freq) # TODO digimodes
|
2018-09-25 12:56:47 +00:00
|
|
|
|
|
|
|
def set_secondary_offset_freq(self, value):
|
2019-07-21 17:40:28 +00:00
|
|
|
self.secondary_offset_freq = value
|
2020-02-25 19:55:42 +00:00
|
|
|
if self.secondary_processes_running and self.has_pipe("secondary_shift_pipe"):
|
2021-01-20 16:01:46 +00:00
|
|
|
self.pipes["secondary_shift_pipe"].write(
|
|
|
|
"%g\n" % (-float(self.secondary_offset_freq) / self.if_samp_rate())
|
|
|
|
)
|
2018-09-25 12:56:47 +00:00
|
|
|
|
|
|
|
def stop_secondary_demodulator(self):
|
2020-02-25 19:55:42 +00:00
|
|
|
if not self.secondary_processes_running:
|
2019-07-21 17:40:28 +00:00
|
|
|
return
|
2018-09-25 12:56:47 +00:00
|
|
|
self.try_delete_pipes(self.secondary_pipe_names)
|
2019-08-17 20:38:09 +00:00
|
|
|
self.try_delete_configs()
|
2019-05-08 14:31:52 +00:00
|
|
|
if self.secondary_process_fft:
|
|
|
|
try:
|
|
|
|
os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM)
|
2020-08-05 18:01:57 +00:00
|
|
|
# drain any leftover data to free file descriptors
|
|
|
|
self.secondary_process_fft.communicate()
|
|
|
|
self.secondary_process_fft = None
|
2019-05-08 14:31:52 +00:00
|
|
|
except ProcessLookupError:
|
|
|
|
# been killed by something else, ignore
|
|
|
|
pass
|
|
|
|
if self.secondary_process_demod:
|
|
|
|
try:
|
|
|
|
os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM)
|
2020-08-05 18:01:57 +00:00
|
|
|
# drain any leftover data to free file descriptors
|
|
|
|
self.secondary_process_demod.communicate()
|
|
|
|
self.secondary_process_demod = None
|
2019-05-08 14:31:52 +00:00
|
|
|
except ProcessLookupError:
|
|
|
|
# been killed by something else, ignore
|
|
|
|
pass
|
2018-09-25 12:56:47 +00:00
|
|
|
self.secondary_processes_running = False
|
|
|
|
|
|
|
|
def get_secondary_demodulator(self):
|
|
|
|
return self.secondary_demodulator
|
|
|
|
|
2019-07-21 17:40:28 +00:00
|
|
|
def set_secondary_fft_size(self, secondary_fft_size):
|
2021-04-05 15:18:30 +00:00
|
|
|
if self.secondary_fft_size == secondary_fft_size:
|
|
|
|
return
|
2019-07-21 17:40:28 +00:00
|
|
|
self.secondary_fft_size = secondary_fft_size
|
2021-04-05 15:18:30 +00:00
|
|
|
self.restart()
|
2018-09-25 12:56:47 +00:00
|
|
|
|
2019-07-21 17:40:28 +00:00
|
|
|
def set_audio_compression(self, what):
|
2021-02-26 23:17:37 +00:00
|
|
|
if self.audio_compression == what:
|
|
|
|
return
|
2018-09-25 12:56:47 +00:00
|
|
|
self.audio_compression = what
|
2021-02-26 23:17:37 +00:00
|
|
|
self.restart()
|
2018-09-25 12:56:47 +00:00
|
|
|
|
2019-10-15 17:50:24 +00:00
|
|
|
def get_audio_bytes_to_read(self):
|
|
|
|
# desired latency: 5ms
|
|
|
|
# uncompressed audio has 16 bits = 2 bytes per sample
|
|
|
|
base = self.output_rate * 0.005 * 2
|
|
|
|
# adpcm compresses the bitstream by 4
|
|
|
|
if self.audio_compression == "adpcm":
|
|
|
|
base = base / 4
|
|
|
|
return int(base)
|
|
|
|
|
2019-07-21 17:40:28 +00:00
|
|
|
def set_fft_compression(self, what):
|
2020-12-19 16:13:36 +00:00
|
|
|
if self.fft_compression == what:
|
|
|
|
return
|
2018-09-25 12:56:47 +00:00
|
|
|
self.fft_compression = what
|
2021-02-26 23:29:04 +00:00
|
|
|
self.restart()
|
2018-09-25 12:56:47 +00:00
|
|
|
|
|
|
|
def get_secondary_fft_bytes_to_read(self):
|
2019-07-21 17:40:28 +00:00
|
|
|
if self.fft_compression == "none":
|
|
|
|
return self.secondary_fft_size * 4
|
|
|
|
if self.fft_compression == "adpcm":
|
|
|
|
return (self.secondary_fft_size / 2) + (10 / 2)
|
2018-09-25 12:56:47 +00:00
|
|
|
|
2019-07-21 17:40:28 +00:00
|
|
|
def set_samp_rate(self, samp_rate):
|
2020-12-19 15:41:48 +00:00
|
|
|
if self.samp_rate == samp_rate:
|
|
|
|
return
|
2019-07-21 17:40:28 +00:00
|
|
|
self.samp_rate = samp_rate
|
2019-05-07 18:06:06 +00:00
|
|
|
self.calculate_decimation()
|
2020-12-20 21:55:10 +00:00
|
|
|
if self.running:
|
2021-07-19 17:04:14 +00:00
|
|
|
self.restart()
|
2019-05-07 18:06:06 +00:00
|
|
|
|
|
|
|
def calculate_decimation(self):
|
2021-01-22 17:47:34 +00:00
|
|
|
(self.decimation, self.last_decimation) = self.get_decimation(self.samp_rate, self.get_audio_rate())
|
2019-05-15 21:08:55 +00:00
|
|
|
|
2019-05-16 19:26:31 +00:00
|
|
|
def get_decimation(self, input_rate, output_rate):
|
2021-02-15 17:03:16 +00:00
|
|
|
if output_rate <= 0:
|
|
|
|
raise ValueError("invalid output rate: {rate}".format(rate=output_rate))
|
2019-07-21 17:40:28 +00:00
|
|
|
decimation = 1
|
2021-01-22 17:47:34 +00:00
|
|
|
target_rate = output_rate
|
2020-08-08 19:56:35 +00:00
|
|
|
# wideband fm has a much higher frequency deviation (75kHz).
|
|
|
|
# we cannot cover this if we immediately decimate to the sample rate the audio will have later on, so we need
|
|
|
|
# to compensate here.
|
2021-01-22 17:47:34 +00:00
|
|
|
if self.get_demodulator() == "wfm" and output_rate < 200000:
|
|
|
|
target_rate = 200000
|
|
|
|
while input_rate / (decimation + 1) >= target_rate:
|
2019-05-15 21:08:55 +00:00
|
|
|
decimation += 1
|
2019-05-16 19:26:31 +00:00
|
|
|
fraction = float(input_rate / decimation) / output_rate
|
2021-01-22 17:47:34 +00:00
|
|
|
return decimation, fraction
|
2018-09-25 12:56:47 +00:00
|
|
|
|
|
|
|
def if_samp_rate(self):
|
2019-07-21 17:40:28 +00:00
|
|
|
return self.samp_rate / self.decimation
|
2018-09-25 12:56:47 +00:00
|
|
|
|
|
|
|
def get_name(self):
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
def get_output_rate(self):
|
|
|
|
return self.output_rate
|
|
|
|
|
2020-08-08 19:29:25 +00:00
|
|
|
def get_hd_output_rate(self):
|
|
|
|
return self.hd_output_rate
|
|
|
|
|
2019-05-16 19:26:31 +00:00
|
|
|
def get_audio_rate(self):
|
2020-09-04 16:09:02 +00:00
|
|
|
if self.isDigitalVoice() or self.isPacket() or self.isPocsag() or self.isDrm():
|
2019-05-16 19:26:31 +00:00
|
|
|
return 48000
|
2020-04-25 17:05:24 +00:00
|
|
|
elif self.isWsjtMode() or self.isJs8():
|
2019-07-06 16:21:43 +00:00
|
|
|
return 12000
|
2020-08-08 19:29:25 +00:00
|
|
|
elif self.isFreeDV():
|
2020-07-27 22:28:20 +00:00
|
|
|
return 8000
|
2020-08-08 19:29:25 +00:00
|
|
|
elif self.isHdAudio():
|
|
|
|
return self.get_hd_output_rate()
|
2019-05-16 19:26:31 +00:00
|
|
|
return self.get_output_rate()
|
|
|
|
|
2019-07-21 17:40:28 +00:00
|
|
|
def isDigitalVoice(self, demodulator=None):
|
2019-05-16 19:26:31 +00:00
|
|
|
if demodulator is None:
|
|
|
|
demodulator = self.get_demodulator()
|
2020-11-23 00:00:25 +00:00
|
|
|
return demodulator in ["dmr", "dstar", "nxdn", "ysf", "m17"]
|
2019-05-16 19:26:31 +00:00
|
|
|
|
2019-07-21 17:40:28 +00:00
|
|
|
def isWsjtMode(self, demodulator=None):
|
2019-07-13 21:16:25 +00:00
|
|
|
if demodulator is None:
|
|
|
|
demodulator = self.get_secondary_demodulator()
|
2021-02-03 19:11:07 +00:00
|
|
|
return demodulator in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]
|
2019-07-13 21:16:25 +00:00
|
|
|
|
2021-01-20 16:01:46 +00:00
|
|
|
def isJs8(self, demodulator=None):
|
2020-04-25 17:05:24 +00:00
|
|
|
if demodulator is None:
|
|
|
|
demodulator = self.get_secondary_demodulator()
|
|
|
|
return demodulator == "js8"
|
2019-07-13 21:16:25 +00:00
|
|
|
|
2019-08-15 17:56:59 +00:00
|
|
|
def isPacket(self, demodulator=None):
|
2019-06-07 13:11:04 +00:00
|
|
|
if demodulator is None:
|
2019-08-11 14:36:53 +00:00
|
|
|
demodulator = self.get_secondary_demodulator()
|
2019-06-07 13:11:04 +00:00
|
|
|
return demodulator == "packet"
|
|
|
|
|
2020-01-06 21:08:17 +00:00
|
|
|
def isPocsag(self, demodulator=None):
|
|
|
|
if demodulator is None:
|
|
|
|
demodulator = self.get_secondary_demodulator()
|
|
|
|
return demodulator == "pocsag"
|
|
|
|
|
2020-08-08 19:29:25 +00:00
|
|
|
def isFreeDV(self, demodulator=None):
|
|
|
|
if demodulator is None:
|
|
|
|
demodulator = self.get_demodulator()
|
|
|
|
return demodulator == "freedv"
|
|
|
|
|
|
|
|
def isHdAudio(self, demodulator=None):
|
|
|
|
if demodulator is None:
|
|
|
|
demodulator = self.get_demodulator()
|
2020-09-04 17:14:16 +00:00
|
|
|
return demodulator == "wfm"
|
2020-09-04 16:09:02 +00:00
|
|
|
|
|
|
|
def isDrm(self, demodulator=None):
|
|
|
|
if demodulator is None:
|
|
|
|
demodulator = self.get_demodulator()
|
|
|
|
return demodulator == "drm"
|
2020-08-08 19:29:25 +00:00
|
|
|
|
2019-07-21 17:40:28 +00:00
|
|
|
def set_output_rate(self, output_rate):
|
2019-10-02 22:14:05 +00:00
|
|
|
if self.output_rate == output_rate:
|
|
|
|
return
|
2019-07-21 17:40:28 +00:00
|
|
|
self.output_rate = output_rate
|
2019-05-07 18:06:06 +00:00
|
|
|
self.calculate_decimation()
|
2019-10-02 22:14:05 +00:00
|
|
|
self.restart()
|
2018-09-25 12:56:47 +00:00
|
|
|
|
2020-08-08 19:29:25 +00:00
|
|
|
def set_hd_output_rate(self, hd_output_rate):
|
|
|
|
if self.hd_output_rate == hd_output_rate:
|
|
|
|
return
|
|
|
|
self.hd_output_rate = hd_output_rate
|
2020-08-08 19:56:35 +00:00
|
|
|
self.calculate_decimation()
|
2020-08-08 19:29:25 +00:00
|
|
|
self.restart()
|
|
|
|
|
2019-07-21 17:40:28 +00:00
|
|
|
def set_demodulator(self, demodulator):
|
2020-04-26 16:45:41 +00:00
|
|
|
if demodulator in ["usb", "lsb", "cw"]:
|
|
|
|
demodulator = "ssb"
|
2019-07-21 17:40:28 +00:00
|
|
|
if self.demodulator == demodulator:
|
|
|
|
return
|
|
|
|
self.demodulator = demodulator
|
2019-05-16 19:26:31 +00:00
|
|
|
self.calculate_decimation()
|
2019-05-08 14:31:52 +00:00
|
|
|
self.restart()
|
2018-09-25 12:56:47 +00:00
|
|
|
|
|
|
|
def get_demodulator(self):
|
|
|
|
return self.demodulator
|
|
|
|
|
2019-07-21 17:40:28 +00:00
|
|
|
def set_offset_freq(self, offset_freq):
|
2020-10-10 22:15:09 +00:00
|
|
|
if offset_freq is None:
|
|
|
|
return
|
2019-07-21 17:40:28 +00:00
|
|
|
self.offset_freq = offset_freq
|
2018-09-25 12:56:47 +00:00
|
|
|
if self.running:
|
2021-07-19 17:04:14 +00:00
|
|
|
if self.pycsdr_chain is not None and isinstance(self.pycsdr_chain, DemodulatorChain):
|
|
|
|
self.pycsdr_chain.setShiftRate(-float(self.offset_freq) / self.samp_rate)
|
|
|
|
else:
|
|
|
|
self.pipes["shift_pipe"].write("%g\n" % (-float(self.offset_freq) / self.samp_rate))
|
|
|
|
|
2018-09-25 12:56:47 +00:00
|
|
|
|
2020-04-05 14:35:46 +00:00
|
|
|
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()
|
|
|
|
self.center_freq = center_freq
|
|
|
|
|
|
|
|
def get_operating_freq(self):
|
|
|
|
return self.center_freq + self.offset_freq
|
2018-09-25 12:56:47 +00:00
|
|
|
|
2020-12-05 23:36:20 +00:00
|
|
|
def set_bandpass(self, bandpass):
|
|
|
|
self.set_bpf(bandpass.low_cut, bandpass.high_cut)
|
|
|
|
|
2019-07-21 17:40:28 +00:00
|
|
|
def set_bpf(self, low_cut, high_cut):
|
|
|
|
self.low_cut = low_cut
|
|
|
|
self.high_cut = high_cut
|
2018-09-25 12:56:47 +00:00
|
|
|
if self.running:
|
2021-07-19 17:04:14 +00:00
|
|
|
if self.pycsdr_chain is not None and isinstance(self.pycsdr_chain, DemodulatorChain):
|
2021-07-19 17:48:18 +00:00
|
|
|
self.pycsdr_chain.setBandpass(float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate())
|
2021-07-19 17:04:14 +00:00
|
|
|
else:
|
|
|
|
self.pipes["bpf_pipe"].write(
|
|
|
|
"%g %g\n" % (float(self.low_cut) / self.if_samp_rate(), float(self.high_cut) / self.if_samp_rate())
|
|
|
|
)
|
2018-09-25 12:56:47 +00:00
|
|
|
|
|
|
|
def get_bpf(self):
|
|
|
|
return [self.low_cut, self.high_cut]
|
|
|
|
|
2019-11-23 15:56:29 +00:00
|
|
|
def convertToLinear(self, db):
|
|
|
|
return float(math.pow(10, db / 10))
|
|
|
|
|
2018-09-25 12:56:47 +00:00
|
|
|
def set_squelch_level(self, squelch_level):
|
2019-07-21 17:40:28 +00:00
|
|
|
self.squelch_level = squelch_level
|
|
|
|
# no squelch required on digital voice modes
|
2021-01-20 16:01:46 +00:00
|
|
|
actual_squelch = (
|
|
|
|
-150
|
2021-07-15 10:54:21 +00:00
|
|
|
if self.isDigitalVoice() or self.isPacket() or self.isPocsag() or self.isFreeDV() or self.isDrm()
|
2021-01-20 16:01:46 +00:00
|
|
|
else self.squelch_level
|
|
|
|
)
|
2018-09-25 12:56:47 +00:00
|
|
|
if self.running:
|
2021-07-19 17:04:14 +00:00
|
|
|
if self.pycsdr_chain is not None and isinstance(self.pycsdr_chain, DemodulatorChain):
|
|
|
|
self.pycsdr_chain.setSquelchLevel(self.convertToLinear(actual_squelch))
|
|
|
|
else:
|
|
|
|
self.pipes["squelch_pipe"].write("%g\n" % (self.convertToLinear(actual_squelch)))
|
2018-09-25 12:56:47 +00:00
|
|
|
|
2021-05-29 16:50:17 +00:00
|
|
|
def set_codecserver(self, s):
|
|
|
|
if self.codecserver == s:
|
|
|
|
return
|
|
|
|
self.codecserver = s
|
|
|
|
self.restart()
|
|
|
|
|
|
|
|
def get_codecserver_arg(self):
|
|
|
|
return "-s {}".format(self.codecserver) if self.codecserver else ""
|
|
|
|
|
2019-06-15 17:10:33 +00:00
|
|
|
def set_dmr_filter(self, filter):
|
2020-02-25 19:55:42 +00:00
|
|
|
if self.has_pipe("dmr_control_pipe"):
|
2020-02-27 23:20:37 +00:00
|
|
|
self.pipes["dmr_control_pipe"].write("{0}\n".format(filter))
|
2018-09-25 12:56:47 +00:00
|
|
|
|
2020-10-04 19:46:58 +00:00
|
|
|
def set_wfm_deemphasis_tau(self, tau):
|
|
|
|
if self.wfm_deemphasis_tau == tau:
|
|
|
|
return
|
|
|
|
self.wfm_deemphasis_tau = tau
|
|
|
|
self.restart()
|
|
|
|
|
2018-09-25 12:56:47 +00:00
|
|
|
def ddc_transition_bw(self):
|
2019-07-21 17:40:28 +00:00
|
|
|
return self.ddc_transition_bw_rate * (self.if_samp_rate() / float(self.samp_rate))
|
2018-09-25 12:56:47 +00:00
|
|
|
|
|
|
|
def try_create_pipes(self, pipe_names, command_base):
|
2020-02-27 23:20:37 +00:00
|
|
|
for pipe_name, pipe_type in pipe_names.items():
|
2020-07-18 18:00:49 +00:00
|
|
|
if self.has_pipe(pipe_name):
|
|
|
|
logger.warning("{pipe_name} is still in use", pipe_name=pipe_name)
|
|
|
|
self.pipes[pipe_name].close()
|
2019-07-21 17:40:28 +00:00
|
|
|
if "{" + pipe_name + "}" in command_base:
|
2020-02-25 19:55:42 +00:00
|
|
|
p = self.pipe_base_path + pipe_name
|
2020-02-27 23:20:37 +00:00
|
|
|
encoding = None
|
|
|
|
# TODO make digiham output unicode and then change this here
|
|
|
|
# the whole pipe enoding feature onlye exists because of this
|
|
|
|
if pipe_name == "meta_pipe":
|
|
|
|
encoding = "cp437"
|
|
|
|
self.pipes[pipe_name] = Pipe.create(p, pipe_type, encoding=encoding)
|
2018-09-25 12:56:47 +00:00
|
|
|
else:
|
2020-02-25 19:55:42 +00:00
|
|
|
self.pipes[pipe_name] = None
|
|
|
|
|
|
|
|
def has_pipe(self, name):
|
|
|
|
return name in self.pipes and self.pipes[name] is not None
|
2018-09-25 12:56:47 +00:00
|
|
|
|
|
|
|
def try_delete_pipes(self, pipe_names):
|
|
|
|
for pipe_name in pipe_names:
|
2020-02-25 19:55:42 +00:00
|
|
|
if self.has_pipe(pipe_name):
|
2020-02-27 23:20:37 +00:00
|
|
|
self.pipes[pipe_name].close()
|
|
|
|
self.pipes[pipe_name] = None
|
2018-09-25 12:56:47 +00:00
|
|
|
|
2019-08-15 23:27:03 +00:00
|
|
|
def try_create_configs(self, command):
|
|
|
|
if "{direwolf_config}" in command:
|
2021-04-07 14:20:10 +00:00
|
|
|
self.direwolf_config_path = "{tmp_dir}/openwebrx_direwolf_{myid}.conf".format(
|
2019-08-17 22:16:08 +00:00
|
|
|
tmp_dir=self.temporary_directory, myid=id(self)
|
|
|
|
)
|
2021-04-07 14:20:10 +00:00
|
|
|
self.direwolf_config = DirewolfConfig()
|
|
|
|
self.direwolf_config.wire(self)
|
|
|
|
file = open(self.direwolf_config_path, "w")
|
|
|
|
file.write(self.direwolf_config.getConfig(self.is_service))
|
2019-08-15 23:27:03 +00:00
|
|
|
file.close()
|
|
|
|
else:
|
|
|
|
self.direwolf_config = None
|
2021-04-07 14:20:10 +00:00
|
|
|
self.direwolf_config_path = None
|
2019-08-17 20:38:09 +00:00
|
|
|
|
|
|
|
def try_delete_configs(self):
|
2021-04-07 14:20:10 +00:00
|
|
|
if self.direwolf_config is not None:
|
|
|
|
self.direwolf_config.unwire(self)
|
|
|
|
self.direwolf_config = None
|
|
|
|
if self.direwolf_config_path is not None:
|
2019-08-17 20:38:09 +00:00
|
|
|
try:
|
2021-04-07 14:20:10 +00:00
|
|
|
os.unlink(self.direwolf_config_path)
|
2019-08-17 20:38:09 +00:00
|
|
|
except FileNotFoundError:
|
|
|
|
# result suits our expectations. fine :)
|
|
|
|
pass
|
|
|
|
except Exception:
|
|
|
|
logger.exception("try_delete_configs()")
|
2021-04-07 14:20:10 +00:00
|
|
|
self.direwolf_config_path = None
|
|
|
|
|
|
|
|
def onConfigChanged(self):
|
|
|
|
self.restart()
|
2019-08-15 23:27:03 +00:00
|
|
|
|
2018-09-25 12:56:47 +00:00
|
|
|
def start(self):
|
2020-02-27 18:48:22 +00:00
|
|
|
with self.modification_lock:
|
|
|
|
if self.running:
|
|
|
|
return
|
|
|
|
self.running = True
|
|
|
|
|
2021-07-19 17:04:14 +00:00
|
|
|
chain = self.chain(self.demodulator)
|
|
|
|
if self.pycsdr_enabled and isinstance(chain, DemodulatorChain):
|
2021-07-19 17:48:18 +00:00
|
|
|
self.set_squelch_level(self.squelch_level)
|
|
|
|
self.set_bpf(self.low_cut, self.high_cut)
|
|
|
|
self.set_offset_freq(self.offset_freq)
|
2021-07-25 17:31:56 +00:00
|
|
|
|
2021-07-19 17:04:14 +00:00
|
|
|
chain.setInput(self.buffer)
|
2021-07-25 17:31:56 +00:00
|
|
|
|
2021-07-25 20:44:53 +00:00
|
|
|
output_rate = self.get_hd_output_rate() if self.isHdAudio() else self.get_output_rate()
|
2021-07-29 22:06:21 +00:00
|
|
|
audio_rate = 8000 if self.isDigitalVoice() else self.get_audio_rate()
|
|
|
|
self.pycsdr_client_chain = ClientAudioChain(chain.getOutputFormat(), audio_rate, output_rate, self.audio_compression)
|
2021-07-25 17:31:56 +00:00
|
|
|
buffer = Buffer(chain.getOutputFormat())
|
|
|
|
chain.setWriter(buffer)
|
|
|
|
self.pycsdr_client_chain.setInput(buffer)
|
|
|
|
|
|
|
|
outputBuffer = Buffer(self.pycsdr_client_chain.getOutputFormat())
|
|
|
|
self.pycsdr_client_chain.setWriter(outputBuffer)
|
2021-07-24 16:50:30 +00:00
|
|
|
self.pycsdr_reader = outputBuffer.getReader()
|
2021-07-25 20:44:53 +00:00
|
|
|
audio_type = "hd_audio" if self.isHdAudio() else "audio"
|
|
|
|
self.output.send_output(audio_type, self.pycsdr_reader.read)
|
2021-07-25 17:31:56 +00:00
|
|
|
|
2021-07-24 22:05:48 +00:00
|
|
|
powerBuffer = Buffer(Format.FLOAT)
|
|
|
|
chain.setPowerWriter(powerBuffer)
|
|
|
|
self.pycsdr_power_reader = powerBuffer.getReader()
|
|
|
|
self.output.send_output("smeter", self.pycsdr_power_reader.read)
|
2021-07-31 22:49:20 +00:00
|
|
|
|
|
|
|
if self.isDigitalVoice():
|
|
|
|
metaBuffer = Buffer(Format.CHAR)
|
|
|
|
chain.setMetaWriter(metaBuffer)
|
|
|
|
self.pycsdr_meta_reader = metaBuffer.getReader()
|
|
|
|
|
|
|
|
def read_meta():
|
|
|
|
raw = self.pycsdr_meta_reader.read()
|
|
|
|
if raw is None or len(raw) == 0:
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
raw = raw.tobytes().decode("cp437")
|
|
|
|
return raw.rstrip("\n")
|
|
|
|
|
|
|
|
self.output.send_output("meta", read_meta)
|
|
|
|
|
2021-07-19 17:04:14 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
command_base = " | ".join(chain)
|
2020-02-27 18:48:22 +00:00
|
|
|
|
|
|
|
# create control pipes for csdr
|
|
|
|
self.try_create_pipes(self.pipe_names, command_base)
|
|
|
|
|
2020-05-06 17:54:55 +00:00
|
|
|
# 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)
|
|
|
|
|
2020-02-27 18:48:22 +00:00
|
|
|
# run the command
|
|
|
|
command = command_base.format(
|
|
|
|
bpf_pipe=self.pipes["bpf_pipe"],
|
|
|
|
shift_pipe=self.pipes["shift_pipe"],
|
|
|
|
squelch_pipe=self.pipes["squelch_pipe"],
|
|
|
|
smeter_pipe=self.pipes["smeter_pipe"],
|
|
|
|
meta_pipe=self.pipes["meta_pipe"],
|
|
|
|
iqtee_pipe=self.pipes["iqtee_pipe"],
|
|
|
|
iqtee2_pipe=self.pipes["iqtee2_pipe"],
|
|
|
|
dmr_control_pipe=self.pipes["dmr_control_pipe"],
|
|
|
|
decimation=self.decimation,
|
|
|
|
last_decimation=self.last_decimation,
|
|
|
|
bpf_transition_bw=float(self.bpf_transition_bw) / self.if_samp_rate(),
|
|
|
|
ddc_transition_bw=self.ddc_transition_bw(),
|
|
|
|
flowcontrol=int(self.samp_rate * 2),
|
|
|
|
start_bufsize=self.base_bufsize * self.decimation,
|
|
|
|
nc_port=self.nc_port,
|
|
|
|
output_rate=self.get_output_rate(),
|
|
|
|
smeter_report_every=int(self.if_samp_rate() / 6000),
|
2021-05-29 16:50:17 +00:00
|
|
|
codecserver_arg=self.get_codecserver_arg(),
|
2020-02-27 18:48:22 +00:00
|
|
|
audio_rate=self.get_audio_rate(),
|
2020-10-04 19:46:58 +00:00
|
|
|
wfm_deemphasis_tau=self.wfm_deemphasis_tau,
|
2019-08-04 12:55:56 +00:00
|
|
|
)
|
2019-05-14 21:30:03 +00:00
|
|
|
|
2020-02-27 18:48:22 +00:00
|
|
|
logger.debug("Command = %s", command)
|
|
|
|
|
|
|
|
out = subprocess.PIPE if self.output.supports_type("audio") else subprocess.DEVNULL
|
2021-02-11 21:51:50 +00:00
|
|
|
self.process = subprocess.Popen(command, stdout=out, shell=True, start_new_session=True)
|
2020-02-27 18:48:22 +00:00
|
|
|
|
|
|
|
def watch_thread():
|
|
|
|
rc = self.process.wait()
|
|
|
|
logger.debug("dsp thread ended with rc=%d", rc)
|
|
|
|
if rc == 0 and self.running and not self.modification_lock.locked():
|
|
|
|
logger.debug("restarting since rc = 0, self.running = true, and no modification")
|
|
|
|
self.restart()
|
|
|
|
|
2020-08-14 18:22:25 +00:00
|
|
|
threading.Thread(target=watch_thread, name="csdr_watch_thread").start()
|
2020-02-27 18:48:22 +00:00
|
|
|
|
2020-08-08 19:29:25 +00:00
|
|
|
audio_type = "hd_audio" if self.isHdAudio() else "audio"
|
|
|
|
if self.output.supports_type(audio_type):
|
2020-02-27 18:48:22 +00:00
|
|
|
self.output.send_output(
|
2020-08-08 19:29:25 +00:00
|
|
|
audio_type,
|
2020-02-27 18:48:22 +00:00
|
|
|
partial(
|
|
|
|
self.process.stdout.read,
|
2021-01-23 18:40:05 +00:00
|
|
|
self.get_audio_bytes_to_read(),
|
2020-02-27 18:48:22 +00:00
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
self.start_secondary_demodulator()
|
|
|
|
|
2020-02-25 19:55:42 +00:00
|
|
|
if self.has_pipe("smeter_pipe"):
|
2021-01-20 16:01:46 +00:00
|
|
|
|
2019-05-14 21:30:03 +00:00
|
|
|
def read_smeter():
|
2020-02-27 23:20:37 +00:00
|
|
|
raw = self.pipes["smeter_pipe"].readline()
|
2019-05-14 21:30:03 +00:00
|
|
|
if len(raw) == 0:
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
return float(raw.rstrip("\n"))
|
2019-07-21 17:40:28 +00:00
|
|
|
|
2019-08-04 12:55:56 +00:00
|
|
|
self.output.send_output("smeter", read_smeter)
|
2020-02-25 19:55:42 +00:00
|
|
|
if self.has_pipe("meta_pipe"):
|
2021-01-20 16:01:46 +00:00
|
|
|
|
2019-05-14 21:30:03 +00:00
|
|
|
def read_meta():
|
2020-02-27 23:20:37 +00:00
|
|
|
raw = self.pipes["meta_pipe"].readline()
|
2019-05-14 21:30:03 +00:00
|
|
|
if len(raw) == 0:
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
return raw.rstrip("\n")
|
2019-07-21 17:40:28 +00:00
|
|
|
|
2019-08-04 12:55:56 +00:00
|
|
|
self.output.send_output("meta", read_meta)
|
2018-09-25 12:56:47 +00:00
|
|
|
|
|
|
|
def stop(self):
|
2020-02-27 18:48:22 +00:00
|
|
|
with self.modification_lock:
|
|
|
|
self.running = False
|
2020-12-16 17:52:00 +00:00
|
|
|
if self.pycsdr_enabled and self.pycsdr_chain is not None:
|
|
|
|
self.pycsdr_chain.stop()
|
2021-01-03 23:24:06 +00:00
|
|
|
self.pycsdr_chain = None
|
2021-07-24 16:50:30 +00:00
|
|
|
self.pycsdr_reader.stop()
|
|
|
|
self.pycsdr_reader = None
|
2021-07-24 22:05:48 +00:00
|
|
|
self.pycsdr_power_reader.stop()
|
|
|
|
self.pycsdr_power_reader = None
|
2021-07-25 17:31:56 +00:00
|
|
|
self.pycsdr_client_chain.stop()
|
|
|
|
self.pycsdr_client_chain = None
|
2021-07-31 22:49:20 +00:00
|
|
|
if self.pycsdr_meta_reader is not None:
|
|
|
|
self.pycsdr_meta_reader.stop()
|
|
|
|
self.pycsdr_meta_reader = None
|
2020-02-27 18:48:22 +00:00
|
|
|
if self.process is not None:
|
|
|
|
try:
|
|
|
|
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
|
2020-08-05 18:01:57 +00:00
|
|
|
# drain any leftover data to free file descriptors
|
|
|
|
self.process.communicate()
|
2020-02-27 18:48:22 +00:00
|
|
|
self.process = None
|
|
|
|
except ProcessLookupError:
|
|
|
|
# been killed by something else, ignore
|
|
|
|
pass
|
|
|
|
self.stop_secondary_demodulator()
|
2018-09-25 12:56:47 +00:00
|
|
|
|
2020-02-27 18:48:22 +00:00
|
|
|
self.try_delete_pipes(self.pipe_names)
|
2021-04-07 14:20:10 +00:00
|
|
|
self.try_delete_configs()
|
2018-09-25 12:56:47 +00:00
|
|
|
|
|
|
|
def restart(self):
|
2019-07-21 17:40:28 +00:00
|
|
|
if not self.running:
|
|
|
|
return
|
2018-09-25 12:56:47 +00:00
|
|
|
self.stop()
|
|
|
|
self.start()
|