From 77f5a9c5084799a7c52f06747c26f0e0ba6ef87e Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Wed, 25 Sep 2024 20:58:53 -0400 Subject: [PATCH] Adding rig control via Hamlib. --- owrx/config/defaults.py | 6 +- owrx/controllers/settings/reporting.py | 29 +- owrx/dsp.py | 5 + owrx/feature.py | 9 + owrx/rigcontrol.py | 359 +++++++++++++++++++++++++ 5 files changed, 404 insertions(+), 4 deletions(-) create mode 100644 owrx/rigcontrol.py diff --git a/owrx/config/defaults.py b/owrx/config/defaults.py index 808b24fd..ed9b4d39 100644 --- a/owrx/config/defaults.py +++ b/owrx/config/defaults.py @@ -363,5 +363,9 @@ defaultConfig = PropertyLayer( cw_showcw=False, dsc_show_errors=True, gps_updates=False, - bandplan_region=0 + bandplan_region=0, + rig_enabled=False, + rig_model=2, + rig_device="127.0.0.1:4533", + rig_address=0 ).readonly() diff --git a/owrx/controllers/settings/reporting.py b/owrx/controllers/settings/reporting.py index ba5ff9ba..d5bcae35 100644 --- a/owrx/controllers/settings/reporting.py +++ b/owrx/controllers/settings/reporting.py @@ -1,11 +1,11 @@ from owrx.controllers.settings import SettingsFormController, SettingsBreadcrumb from owrx.form.section import Section -from owrx.form.input.converter import OptionalConverter +from owrx.form.input.converter import OptionalConverter, IntConverter from owrx.form.input.aprs import AprsBeaconSymbols, AprsAntennaDirections -from owrx.form.input import TextInput, CheckboxInput, DropdownInput, NumberInput, PasswordInput +from owrx.form.input import TextInput, CheckboxInput, DropdownInput, NumberInput, PasswordInput, Option from owrx.form.input.validator import AddressAndOptionalPortValidator from owrx.breadcrumb import Breadcrumb, BreadcrumbItem - +from owrx.rigcontrol import RigControl class ReportingController(SettingsFormController): def getTitle(self): @@ -128,5 +128,28 @@ class ReportingController(SettingsFormController): infotext="MQTT topic to publish reports to (default: openwebrx)", converter=OptionalConverter(), ), + ), + Section( + "RigControl settings", + CheckboxInput( + "rig_enabled", + "Enable sending changes to a standalone transceiver", + ), + DropdownInput( + "rig_model", + "Transceiver model", + options=[Option(str(RigControl.RIGS[x]), x) for x in RigControl.RIGS.keys()], + converter=IntConverter(), + ), + TextInput( + "rig_device", + "Transceiver CAT device", + infotext="Device or IP address:port used to control transceiver", + ), + NumberInput( + "rig_address", + "Transceiver CI-V address", + infotext="Optional transceiver CI-V address (used by Icom)", + ), ) ] diff --git a/owrx/dsp.py b/owrx/dsp.py index 9e733bc7..72167b0a 100644 --- a/owrx/dsp.py +++ b/owrx/dsp.py @@ -2,6 +2,7 @@ from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass from owrx.property import PropertyStack, PropertyLayer, PropertyValidator, PropertyDeleted, PropertyDeletion from owrx.property.validators import OrValidator, RegexValidator, BoolValidator from owrx.modes import Modes, DigitalMode +from owrx.rigcontrol import RigControl from csdr.chain import Chain from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, \ SecondaryDemodulator, DialFrequencyReceiver, MetaProvider, SlotFilterChain, SecondarySelectorChain, \ @@ -569,6 +570,9 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient) self.sdrSource.addClient(self) + self.rigControl = RigControl(self.props) + + def setSecondaryFftSize(self, size): self.chain.setSecondaryFftSize(size) self.handler.write_secondary_dsp_config({"secondary_fft_size": size}) @@ -854,6 +858,7 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient) for sub in self.subscriptions: sub.cancel() self.subscriptions = [] + self.rigControl.stop() def setProperties(self, props): for k, v in props.items(): diff --git a/owrx/feature.py b/owrx/feature.py index 8326db04..b3f9c254 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -100,6 +100,7 @@ class FeatureDetector(object): "dab": ["csdreti", "dablin"], "mqtt": ["paho_mqtt"], "hdradio": ["nrsc5"], + "rigcontrol": ["hamlib"], } def feature_availability(self): @@ -766,3 +767,11 @@ class FeatureDetector(object): from the OpenWebRX+ repositories. """ return self.command_is_runnable("nrsc5 -v") + + def has_hamlib(self): + """ + OpenWebRX uses the [Hamlib](https://github.com/Hamlib/Hamlib) `rigctl` + tool to synchronize frequency and modulation with external transceivers. + The `hamlib` package is available in most Linux distributions. + """ + return self.command_is_runnable("rigctl -V") diff --git a/owrx/rigcontrol.py b/owrx/rigcontrol.py new file mode 100644 index 00000000..6bf23338 --- /dev/null +++ b/owrx/rigcontrol.py @@ -0,0 +1,359 @@ +from owrx.feature import FeatureDetector +from owrx.property import PropertyStack +from owrx.config import Config + +import subprocess +import logging + +logger = logging.getLogger(__name__) + +class RigControl(): + 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, + } + + MODES = { + "nfm" : "FM", "wfm" : "WFM", + "am" : "AM", "sam" : "SAM", + "lsb" : "LSB", "usb" : "USB", + "cw" : "CW", + } + + def __init__(self, props: PropertyStack): + self.mod = None + self.fCenter = 0 + self.fOffset = 0 + self.subscriptions = [ + props.wireProperty("offset_freq", self.setFrequencyOffset), + props.wireProperty("center_freq", self.setCenterFrequency), + props.wireProperty("mod", self.setDemodulator), + ] + super().__init__() + + def stop(self): + for sub in self.subscriptions: + sub.cancel() + self.subscriptions = [] + + def setFrequencyOffset(self, offset: int) -> None: + if offset != self.fOffset and self.rigFrequency(self.fCenter + offset): + self.fOffset = offset + + def setCenterFrequency(self, center: int) -> None: + if center != self.fCenter and self.rigFrequency(center + self.fOffset): + self.fCenter = center + + def setDemodulator(self, mod: str) -> None: + if mod != self.mod and self.rigModulation(mod): + self.mod = mod + + def rigTX(self, active: bool) -> bool: + return self.rigCommand(["set_ptt", "1" if active else "0"]) + + def rigFrequency(self, freq: int) -> bool: + return self.rigCommand(["set_freq", str(freq)]) + + def rigModulation(self, mod: str) -> bool: + if mod in self.MODES: + return self.rigCommand(["set_mode", self.MODES[mod], "0"]) + else: + return False + + def rigCommand(self, cmd: list[str]) -> bool: + # 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 + rigctl = ["rigctl", "-m", str(pm["rig_model"]), "-r", pm["rig_device"]] + address = pm["rig_address"] + if address > 0 and address < 256: + rigctl += ["-c", str(address)] + # Rigctl must return 0 to indicate success + return subprocess.run(rigctl + cmd).returncode == 0