443 lines
15 KiB
Python
443 lines
15 KiB
Python
from owrx.feature import FeatureDetector
|
|
from owrx.property import PropertyStack
|
|
from owrx.config import Config
|
|
from subprocess import Popen, PIPE, STDOUT, TimeoutExpired
|
|
|
|
import threading
|
|
import select
|
|
import os
|
|
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class RigControl():
|
|
# Mapping from rig names to Rigctl rig types
|
|
RIGS = {
|
|
# "Hamlib Dummy" : 1,
|
|
"Hamlib" : 2,
|
|
"FLRig" : 4,
|
|
"TRXManager 5.7.630+" : 5,
|
|
# "Hamlib Dummy No VFO" : 6,
|
|
|
|
"ADAT ADT-200A" : 29001,
|
|
"AE9RB Si570 Peaberry V1" : 25016,
|
|
"AE9RB Si570 Peaberry V2" : 25017,
|
|
"Alinco DX-77" : 17001,
|
|
"Alinco DX-SR8" : 17002,
|
|
"AmQRP DDS-60" : 25006,
|
|
"AMSAT-UK FUNcube Dongle" : 25013,
|
|
"AMSAT-UK FUNcube Dongle Pro+" : 25018,
|
|
"ANAN Thetis" : 2048,
|
|
|
|
"AOR AR3000A" : 5006,
|
|
"AOR AR3030" : 5005,
|
|
"AOR AR5000" : 5004,
|
|
"AOR AR2700" : 5008,
|
|
"AOR AR8600" : 5013,
|
|
"AOR AR5000A" : 5014,
|
|
"AOR AR7030" : 5003,
|
|
"AOR AR7030 Plus" : 5015,
|
|
"AOR AR8000" : 5002,
|
|
"AOR AR8200" : 5001,
|
|
"AOR SR2200" : 5016,
|
|
|
|
"Barrett 2050" : 32001,
|
|
"Barrett 950" : 32002,
|
|
"Coding Technologies Digital World Traveller" : 25003,
|
|
"Dorji DRA818V" : 31001,
|
|
"Dorji DRA818U" : 31002,
|
|
"Drake R-8A" : 9002,
|
|
"Drake R-8B" : 9003,
|
|
"DTTS Microwave Society DttSP IPC" : 23003,
|
|
"DTTS Microwave Society DttSP UDP" : 23004,
|
|
"ELAD FDM-DUO" : 33001,
|
|
|
|
"Elecraft K2" : 2021,
|
|
"Elecraft K3" : 2029,
|
|
"Elecraft K3S" : 2043,
|
|
"Elecraft K4" : 2047,
|
|
"Elecraft KX2" : 2044,
|
|
"Elecraft KX3" : 2045,
|
|
"Elecraft XG3" : 2038,
|
|
|
|
"Elektor SDR-USB" : 25007,
|
|
"Elektor 3/04" : 25001,
|
|
"FiFi FiFi-SDR" : 25012,
|
|
"FlexRadio 6xxx" : 2036,
|
|
"FlexRadio PowerSDR" : 2048,
|
|
"FlexRadio SDR-1000" : 23001,
|
|
"Funkamateur FA-SDR" : 25015,
|
|
"Hilberling PT-8000A" : 2046,
|
|
"HobbyPCB RS-HFIQ" : 25019,
|
|
|
|
"Icom IC-92D" : 3065,
|
|
"Icom IC-271" : 3003,
|
|
"Icom IC-275" : 3004,
|
|
"Icom IC-471" : 3006,
|
|
"Icom IC-475" : 3007,
|
|
"Icom IC-575" : 3008,
|
|
"Icom IC-703" : 3055,
|
|
"Icom IC-706" : 3009,
|
|
"Icom IC-706MkII" : 3010,
|
|
"Icom IC-706MkIIG" : 3011,
|
|
"Icom IC-705" : 3085,
|
|
"Icom IC-707" : 3012,
|
|
"Icom IC-718" : 3013,
|
|
"Icom IC-725" : 3014,
|
|
"Icom IC-726" : 3015,
|
|
"Icom IC-728" : 3016,
|
|
"Icom IC-729" : 3017,
|
|
"Icom IC-735" : 3019,
|
|
"Icom IC-736" : 3020,
|
|
"Icom IC-737" : 3021,
|
|
"Icom IC-738" : 3022,
|
|
"Icom IC-746" : 3023,
|
|
"Icom IC-746PRO" : 3046,
|
|
"Icom IC-751" : 3024,
|
|
"Icom IC-756" : 3026,
|
|
"Icom IC-756PRO" : 3027,
|
|
"Icom IC-756PROII" : 3047,
|
|
"Icom IC-756PROIII" : 3057,
|
|
"Icom IC-761" : 3028,
|
|
"Icom IC-765" : 3029,
|
|
"Icom IC-775" : 3030,
|
|
"Icom IC-78" : 3045,
|
|
"Icom IC-781" : 3031,
|
|
"Icom IC-820H" : 3032,
|
|
"Icom IC-821H" : 3034,
|
|
"Icom IC-910" : 3044,
|
|
"Icom IC-970" : 3035,
|
|
"Icom IC-1275" : 3002,
|
|
"Icom IC-2730" : 3072,
|
|
"Icom IC-7000" : 3060,
|
|
"Icom IC-7100" : 3070,
|
|
"Icom IC-7300" : 3073,
|
|
"Icom IC-7200" : 3061,
|
|
"Icom IC-7410" : 3067,
|
|
"Icom IC-7700" : 3062,
|
|
"Icom IC-7600" : 3063,
|
|
"Icom IC-7610" : 3078,
|
|
"Icom IC-7800" : 3056,
|
|
"Icom IC-785x" : 3075,
|
|
"Icom IC-9100" : 3068,
|
|
"Icom IC-9700" : 3081,
|
|
|
|
"Icom IC-M700PRO" : 30001,
|
|
"Icom IC-M710" : 30003,
|
|
"Icom IC-M802" : 30002,
|
|
"Icom IC-M803" : 30004,
|
|
|
|
"Icom IC-R6" : 3077,
|
|
"Icom IC-R10" : 3036,
|
|
"Icom IC-R20" : 3058,
|
|
"Icom IC-R30" : 3080,
|
|
"Icom IC-R71" : 3037,
|
|
"Icom IC-R72" : 3038,
|
|
"Icom IC-R75" : 3039,
|
|
"Icom IC-R7000" : 3040,
|
|
"Icom IC-R7100" : 3041,
|
|
"Icom IC-R8500" : 3042,
|
|
"Icom IC-R8600" : 3079,
|
|
"Icom IC-R9000" : 3043,
|
|
"Icom IC-R9500" : 3066,
|
|
"Icom IC-RX7" : 3069,
|
|
|
|
"Icom IC-PCR1000" : 4001,
|
|
"Icom IC-PCR100" : 4002,
|
|
"Icom IC-PCR1500" : 4003,
|
|
"Icom IC-PCR2500" : 4004,
|
|
|
|
"Icom ID-1" : 3054,
|
|
"Icom ID-31" : 3083,
|
|
"Icom ID-51" : 3084,
|
|
"Icom ID-4100" : 3082,
|
|
"Icom ID-5100" : 3071,
|
|
|
|
"JRC NRD-525" : 6005,
|
|
"JRC NRD-535D" : 6006,
|
|
"JRC NRD-545 DSP" : 6007,
|
|
"Kachina 505DSP" : 18001,
|
|
|
|
"Kenwood R-5000" : 2015,
|
|
"Kenwood TH-D7A" : 2017,
|
|
"Kenwood TH-D72A" : 2033,
|
|
"Kenwood TH-D74" : 2042,
|
|
"Kenwood TH-F6A" : 2019,
|
|
"Kenwood TH-F7E" : 2020,
|
|
"Kenwood TH-G71" : 2023,
|
|
"Kenwood TM-D700" : 2026,
|
|
"Kenwood TM-D710(G)" : 2034,
|
|
"Kenwood TM-V7" : 2027,
|
|
"Kenwood TRC-80" : 2030,
|
|
"Kenwood TS-50S" : 2001,
|
|
"Kenwood TS-440S" : 2002,
|
|
"Kenwood TS-450S" : 2003,
|
|
"Kenwood TS-480" : 2028,
|
|
"Kenwood TS-570D" : 2004,
|
|
"Kenwood TS-570S" : 2016,
|
|
"Kenwood TS-590S" : 2031,
|
|
"Kenwood TS-590SG" : 2037,
|
|
"Kenwood TS-690S" : 2005,
|
|
"Kenwood TS-711" : 2006,
|
|
"Kenwood TS-790" : 2007,
|
|
"Kenwood TS-811" : 2008,
|
|
"Kenwood TS-850" : 2009,
|
|
"Kenwood TS-870S" : 2010,
|
|
"Kenwood TS-890S" : 2041,
|
|
"Kenwood TS-940S" : 2011,
|
|
"Kenwood TS-950S" : 2012,
|
|
"Kenwood TS-950SDX" : 2013,
|
|
"Kenwood TS-990S" : 2039,
|
|
"Kenwood TS-2000" : 2014,
|
|
"Kenwood TS-930" : 2022,
|
|
"Kenwood TS-680S" : 2024,
|
|
"Kenwood TS-140S" : 2025,
|
|
|
|
"KTH-SDR Si570 PIC-USB" : 25011,
|
|
"Lowe HF-235" : 10004,
|
|
"Malachite DSP" : 2049,
|
|
"Microtelecom Perseus" : 3074,
|
|
"mRS miniVNA" : 25008,
|
|
"N2ADR HiQSDR" : 25014,
|
|
"OpenHPSDR PiHPSDR" : 2040,
|
|
|
|
"Optoelectronics OptoScan535" : 3052,
|
|
"Optoelectronics OptoScan456" : 3053,
|
|
|
|
"Philips/Simoco PRM8060" : 28001,
|
|
"Racal RA3702" : 11005,
|
|
"Racal RA6790/GM" : 11003,
|
|
"RadioShack PRO-2052" : 8004,
|
|
"RFT EKD-500" : 24001,
|
|
"Rohde & Schwarz EB200" : 27002,
|
|
"Rohde & Schwarz ESMC" : 27001,
|
|
"Rohde & Schwarz XK2100" : 27003,
|
|
"SAT-Schneider DRT1" : 25002,
|
|
"SigFox Transfox" : 2032,
|
|
"Skanti TRP8000" : 14002,
|
|
"Skanti TRP8255SR" : 14004,
|
|
"SoftRock Si570 AVR-USB" : 25009,
|
|
"TAPR DSP-10" : 22001,
|
|
|
|
"Ten-Tec Delta II" : 3064,
|
|
"Ten-Tec Omni VI Plus" : 3051,
|
|
"Ten-Tec RX-320" : 16003,
|
|
"Ten-Tec RX-331" : 16012,
|
|
"Ten-Tec RX-340" : 16004,
|
|
"Ten-Tec RX-350" : 16005,
|
|
"Ten-Tec TT-516 Argonaut V" : 16007,
|
|
"Ten-Tec TT-538 Jupiter" : 16002,
|
|
"Ten-Tec TT-550" : 16001,
|
|
"Ten-Tec TT-565 Orion" : 16008,
|
|
"Ten-Tec TT-585 Paragon" : 16009,
|
|
"Ten-Tec TT-588 Omni VII" : 16011,
|
|
"Ten-Tec TT-599 Eagle" : 16013,
|
|
|
|
"Uniden BC245xlt" : 8002,
|
|
"Uniden BC250D" : 8006,
|
|
"Uniden BC780xlt" : 8001,
|
|
"Uniden BC895xlt" : 8003,
|
|
"Uniden BC898T" : 8012,
|
|
"Uniden BCD-396T" : 8010,
|
|
"Uniden BCD-996T" : 8011,
|
|
|
|
"Vertex Standard VX-1700" : 1033,
|
|
"Video4Linux SW/FM Radio" : 26001,
|
|
"Video4Linux2 SW/FM Radio" : 26002,
|
|
"Watkins-Johnson WJ-8888" : 12004,
|
|
|
|
"Winradio WR-1000" : 15001,
|
|
"Winradio WR-1500" : 15002,
|
|
"Winradio WR-1550" : 15003,
|
|
"Winradio WR-3100" : 15004,
|
|
"Winradio WR-3150" : 15005,
|
|
"Winradio WR-3500" : 15006,
|
|
"Winradio WR-3700" : 15007,
|
|
"Winradio WR-G313" : 15009,
|
|
|
|
"Xiegu X108G" : 3076,
|
|
|
|
"Yaesu FRG-100" : 1017,
|
|
"Yaesu FRG-8800" : 1019,
|
|
"Yaesu FRG-9600" : 1018,
|
|
"Yaesu FT-100" : 1021,
|
|
"Yaesu FT-450" : 1027,
|
|
"Yaesu FT-600" : 1039,
|
|
"Yaesu FT-736R" : 1010,
|
|
"Yaesu FT-747GX" : 1005,
|
|
"Yaesu FT-757GX" : 1006,
|
|
"Yaesu FT-757GXII" : 1007,
|
|
"Yaesu FT-767GX" : 1009,
|
|
"Yaesu FT-817" : 1020,
|
|
"Yaesu FT-818" : 1041,
|
|
"Yaesu FT-840" : 1011,
|
|
"Yaesu FT-847" : 1001,
|
|
"Yaesu FT-847UNI" : 1038,
|
|
"Yaesu FT-857" : 1022,
|
|
"Yaesu FT-890" : 1015,
|
|
"Yaesu FT-891" : 1036,
|
|
"Yaesu FT-897" : 1023,
|
|
"Yaesu FT-897D" : 1043,
|
|
"Yaesu FT-900" : 1013,
|
|
"Yaesu FT-920" : 1014,
|
|
"Yaesu FT-950" : 1028,
|
|
"Yaesu FT-980" : 1031,
|
|
"Yaesu FT-990" : 1016,
|
|
"Yaesu FT-991" : 1035,
|
|
"Yaesu FT-1000D" : 1003,
|
|
"Yaesu FT-1000MP" : 1024,
|
|
"Yaesu FT-1000MP Mark-V" : 1004,
|
|
"Yaesu FT-1000MP Mark-V Field" : 1025,
|
|
"Yaesu FT-2000" : 1029,
|
|
"Yaesu FTDX-10" : 1042,
|
|
"Yaesu FTDX-101D" : 1040,
|
|
"Yaesu FTDX-101MP" : 1044,
|
|
"Yaesu FTDX-1200" : 1034,
|
|
"Yaesu FTDX-3000" : 1037,
|
|
"Yaesu FTDX-5000" : 1032,
|
|
"Yaesu FTDX-9000" : 1030,
|
|
"Yaesu VR-5000" : 1026,
|
|
}
|
|
|
|
# Mapping from OpenWebRX modulations to Rigctl modulations
|
|
MODES = {
|
|
"nfm" : "FM", "wfm" : "WFM",
|
|
"am" : "AM", "sam" : "SAM",
|
|
"lsb" : "LSB", "usb" : "USB",
|
|
"lsbd" : "PKTLSB", "usbd" : "PKTUSB",
|
|
"cw" : "CWR",
|
|
}
|
|
|
|
def __init__(self, props: PropertyStack):
|
|
self.rigctl = None
|
|
self.thread = None
|
|
self.mod = None
|
|
self.fCenter = None
|
|
self.fOffset = None
|
|
self.subscriptions = [
|
|
props.wireProperty("offset_freq", self.setFrequencyOffset),
|
|
props.wireProperty("center_freq", self.setCenterFrequency),
|
|
props.wireProperty("mod", self.setDemodulator),
|
|
]
|
|
super().__init__()
|
|
self.rigStart()
|
|
|
|
def stop(self):
|
|
for sub in self.subscriptions:
|
|
sub.cancel()
|
|
self.subscriptions = []
|
|
self.rigStop()
|
|
|
|
def setFrequencyOffset(self, offset: int) -> None:
|
|
if self.fCenter is not None and offset != self.fOffset:
|
|
if self.rigFrequency(self.fCenter + offset):
|
|
self.fOffset = offset
|
|
|
|
def setCenterFrequency(self, center: int) -> None:
|
|
self.fCenter = center
|
|
self.fOffset = None
|
|
|
|
def setDemodulator(self, mod: str) -> None:
|
|
if mod != self.mod and self.rigModulation(mod):
|
|
self.mod = mod
|
|
|
|
# Press or release rig's PTT (i.e. transmit)
|
|
def rigTX(self, active: bool) -> bool:
|
|
return self.rigCommand("T {0}".format(1 if active else 0))
|
|
|
|
# Set rig's frequency
|
|
def rigFrequency(self, freq: int) -> bool:
|
|
return self.rigCommand("F {0}".format(freq))
|
|
|
|
# Set rig's modulation
|
|
def rigModulation(self, mod: str) -> bool:
|
|
if mod in self.MODES:
|
|
return self.rigCommand("M {0} 0".format(self.MODES[mod]))
|
|
else:
|
|
return False
|
|
|
|
# Start Rigctl and associated thread
|
|
def rigStart(self):
|
|
# Do not start twice
|
|
if self.rigctl is not None:
|
|
return True
|
|
# Must have Hamlib/Rigctl installed
|
|
if not FeatureDetector().is_available("rigcontrol"):
|
|
return False
|
|
# Must have rig control enabled
|
|
pm = Config.get()
|
|
if not pm["rig_enabled"]:
|
|
return False
|
|
# Compose Rigctl command
|
|
address = pm["rig_address"]
|
|
cmd = [
|
|
"rigctl", "-m", str(pm["rig_model"]), "-r", pm["rig_device"]
|
|
] + (
|
|
["-c", str(address)] if address > 0 and address < 256 else []
|
|
) + ["-"]
|
|
#cmd = ["rigctl", "-"] # @@@ REMOVE ME!!!!
|
|
# Create Rigctl process, make stdout/stderr pipes non-blocking
|
|
self.rigctl = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, text=True)
|
|
os.set_blocking(self.rigctl.stdout.fileno(), False)
|
|
os.set_blocking(self.rigctl.stderr.fileno(), False)
|
|
# Create and start thread
|
|
self.thread = threading.Thread(target=self._rigThread)
|
|
self.thread.start()
|
|
# Done
|
|
logger.debug("Started RigControl as '{0}'.".format(" ".join(cmd)))
|
|
return True
|
|
|
|
# Stop Rigctl and associated thread
|
|
def rigStop(self):
|
|
# Do not stop twice
|
|
if self.rigctl is None:
|
|
return
|
|
# If Rigctl still running...
|
|
if self.rigctl.poll() is None:
|
|
# Try terminating Rigctl normally, kill if failed
|
|
logger.info("Stopping RigControl executable...")
|
|
try:
|
|
self.rigctl.terminate()
|
|
self.rigctl.wait(3)
|
|
except TimeoutExpired:
|
|
self.rigctl.kill()
|
|
# The thread should have exited, since Rigctl exited
|
|
logger.info("Waiting for RigControl thread...")
|
|
self.thread.join()
|
|
logger.info("Stopped RigControl.")
|
|
self.thread = None
|
|
self.rigctl = None
|
|
|
|
# Send command to Rigctl
|
|
def rigCommand(self, cmd: str) -> bool:
|
|
if self.rigctl is not None:
|
|
if self.rigctl.poll() is not None:
|
|
self.rigctl = None
|
|
return False
|
|
try:
|
|
self.rigctl.stdin.write(cmd + "\n")
|
|
self.rigctl.stdin.flush()
|
|
logger.debug("Sent '{0}' to RigControl.".format(cmd))
|
|
return True
|
|
except Exception as e:
|
|
logger.debug("Failed sending '{0}' to RigControl: {1}.".format(cmd, str(e)))
|
|
# Failed to send command
|
|
return False
|
|
|
|
# This thread function reads from Rigctl process' stdout/stderr
|
|
def _rigThread(self):
|
|
# While process is running...
|
|
while self.rigctl.poll() is None:
|
|
try:
|
|
# Wait for output from the process
|
|
readable, _, _ = select.select([self.rigctl.stdout, self.rigctl.stderr], [], [])
|
|
for pipe in readable:
|
|
rsp = pipe.read().strip()
|
|
logger.debug("STD{0}: {1}".format("ERR" if pipe==self.rigctl.stderr else "OUT", rsp))
|
|
except Exception as e:
|
|
logger.debug("Failed receiving from RigControl: {1}.".format(str(e)))
|
|
|
|
# Process stopped
|
|
logger.debug("RigControl process quit ({0}).".format(self.rigctl.poll()))
|