From 543cbbdff81d84c8d0366f87d15e759838682dbc Mon Sep 17 00:00:00 2001 From: DJ2LS Date: Fri, 12 Jan 2024 15:51:23 +0100 Subject: [PATCH] RADIO MANAGER - WIP initial release --- modem/modem.py | 74 +----- modem/radio_manager.py | 56 +++++ modem/rigctld.py | 472 +++++++++++++-------------------------- modem/rigdummy.py | 29 ++- modem/server.py | 2 + modem/service_manager.py | 45 ++-- tests/test_protocols.py | 5 +- 7 files changed, 276 insertions(+), 407 deletions(-) create mode 100644 modem/radio_manager.py diff --git a/modem/modem.py b/modem/modem.py index ef9af3e1..55393c40 100644 --- a/modem/modem.py +++ b/modem/modem.py @@ -31,11 +31,12 @@ class RF: log = structlog.get_logger("RF") - def __init__(self, config, event_manager, fft_queue, service_queue, states) -> None: + def __init__(self, config, event_manager, fft_queue, service_queue, states, radio_manager) -> None: self.config = config self.service_queue = service_queue self.states = states self.event_manager = event_manager + self.radio = radio_manager self.sampler_avg = 0 self.buffer_avg = 0 @@ -108,7 +109,6 @@ class RF: # Initialize codec2, rig control, and data threads self.init_codec2() - self.init_rig_control() self.init_data_threads() return True @@ -423,73 +423,3 @@ class RF: else: sd.play(audio_48k, blocksize=4096, blocking=True) return - - def init_rig_control(self): - # Check how we want to control the radio - if self.radiocontrol == "rigctld": - import rigctld as rig - elif self.radiocontrol == "tci": - self.radio = self.tci_module - else: - import rigdummy as rig - - if not self.radiocontrol in ["tci"]: - self.radio = rig.radio() - self.radio.open_rig( - rigctld_ip=self.rigctld_ip, - rigctld_port=self.rigctld_port, - ) - hamlib_thread = threading.Thread( - target=self.update_rig_data, name="HAMLIB_THREAD", daemon=True - ) - hamlib_thread.start() - - hamlib_set_thread = threading.Thread( - target=self.set_rig_data, name="HAMLIB_SET_THREAD", daemon=True - ) - hamlib_set_thread.start() - - def set_rig_data(self) -> None: - """ - Set rigctld parameters like frequency, mode - THis needs to be processed in a queue - """ - while True: - cmd = RIGCTLD_COMMAND_QUEUE.get() - if cmd[0] == "set_frequency": - # [1] = Frequency - self.radio.set_frequency(cmd[1]) - if cmd[0] == "set_mode": - # [1] = Mode - self.radio.set_mode(cmd[1]) - - def update_rig_data(self) -> None: - """ - Request information about the current state of the radio via hamlib - """ - while True: - try: - # this looks weird, but is necessary for avoiding rigctld packet colission sock - #threading.Event().wait(0.1) - self.states.set("radio_status", self.radio.get_status()) - #threading.Event().wait(0.25) - self.states.set("radio_frequency", self.radio.get_frequency()) - threading.Event().wait(0.1) - self.states.set("radio_mode", self.radio.get_mode()) - threading.Event().wait(0.1) - self.states.set("radio_bandwidth", self.radio.get_bandwidth()) - threading.Event().wait(0.1) - if self.states.isTransmitting(): - self.radio_alc = self.radio.get_alc() - threading.Event().wait(0.1) - self.states.set("radio_rf_power", self.radio.get_level()) - threading.Event().wait(0.1) - self.states.set("radio_strength", self.radio.get_strength()) - - except Exception as e: - self.log.warning( - "[MDM] error getting radio data", - e=e, - ) - threading.Event().wait(1) - diff --git a/modem/radio_manager.py b/modem/radio_manager.py new file mode 100644 index 00000000..1ddbc68b --- /dev/null +++ b/modem/radio_manager.py @@ -0,0 +1,56 @@ +import rigctld +import tci +import rigdummy +import time +import threading +class RadioManager: + def __init__(self, config, state_manager, event_manager): + self.config = config + self.state_manager = state_manager + self.event_manager = event_manager + + self.radiocontrol = config['RADIO']['control'] + self.rigctld_ip = config['RIGCTLD']['ip'] + self.rigctld_port = config['RIGCTLD']['port'] + + self.refresh_rate = 1 + self.stop_event = threading.Event() + self.update_thread = threading.Thread(target=self.update_parameters, daemon=True) + self._init_rig_control() + + def _init_rig_control(self): + # Check how we want to control the radio + if self.radiocontrol == "rigctld": + self.radio = rigctld.radio(self.state_manager, hostname=self.rigctld_ip,port=self.rigctld_port) + elif self.radiocontrol == "tci": + raise NotImplementedError + # self.radio = self.tci_module + else: + self.radio = rigdummy.radio() + + self.update_thread.start() + + def set_ptt(self, state): + self.radio.set_ptt(state) + + def set_frequency(self, frequency): + self.radio.set_frequency(frequency) + + def set_mode(self, mode): + self.radio.set_mode(mode) + + def update_parameters(self): + while not self.stop_event.is_set(): + parameters = self.radio.get_parameters() + self.state_manager.set("radio_frequency", parameters['frequency']) + self.state_manager.set("radio_mode", parameters['mode']) + self.state_manager.set("radio_bandwidth", parameters['bandwidth']) + self.state_manager.set("radio_rf_power", parameters['rf']) + + if self.state_manager.isTransmitting(): + self.radio_alc = parameters['alc'] + self.state_manager.set("radio_strength", parameters['strength']) + time.sleep(self.refresh_rate) + def stop(self): + self.radio.disconnect() + self.stop_event.set() \ No newline at end of file diff --git a/modem/rigctld.py b/modem/rigctld.py index 6d761b00..ef47651d 100644 --- a/modem/rigctld.py +++ b/modem/rigctld.py @@ -1,345 +1,189 @@ -#!/usr/bin/env python3 -# class taken from darksidelemm -# rigctl - https://github.com/darksidelemm/rotctld-web-gui/blob/master/rotatorgui.py#L35 -# -# modified and adjusted to FreeDATA needs by DJ2LS - -import contextlib import socket import structlog -import threading class radio: """rigctld (hamlib) communication class""" log = structlog.get_logger("radio (rigctld)") - def __init__(self, hostname="localhost", port=4532, poll_rate=5, timeout=5): - """Open a connection to rigctld, and test it for validity""" - self.ptt_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.data_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - self.ptt_connected = False - self.data_connected = False + def __init__(self, states, hostname="localhost", port=4532, timeout=5): self.hostname = hostname self.port = port - self.connection_attempts = 5 + self.timeout = timeout + self.states = states - # class wide variable for some parameters - self.bandwidth = '' - self.frequency = '' - self.mode = '' - self.alc = '' - self.strength = '' - self.rf = '' + self.connection = None + self.connected = False - def open_rig( - self, - rigctld_ip, - rigctld_port - ): - """ + self.parameters = { + 'frequency': '---', + 'mode': '---', + 'alc': '---', + 'strength': '---', + 'bandwidth': '---', + 'rf': '---', + 'ptt': False # Initial PTT state is set to False + } - Args: - rigctld_ip: - rigctld_port: + # connect to radio + self.connect() - Returns: + def connect(self): + try: + self.connection = socket.create_connection((self.hostname, self.port), timeout=self.timeout) + self.connected = True + self.states.set("radio_status", True) + self.log.info(f"[RIGCTLD] Connected to rigctld at {self.hostname}:{self.port}") + except Exception as err: + self.log.warning(f"[RIGCTLD] Failed to connect to rigctld: {err}") + self.connected = False + self.states.set("radio_status", False) - """ - self.hostname = rigctld_ip - self.port = int(rigctld_port) + def disconnect(self): + self.connected = False + self.connection.close() + del self.connection + self.connection = None + self.states.set("radio_status", False) + self.parameters = { + 'frequency': '---', + 'mode': '---', + 'alc': '---', + 'strength': '---', + 'bandwidth': '---', + 'rf': '---', + 'ptt': False # Initial PTT state is set to False + } - # _ptt_connect = self.ptt_connect() - # _data_connect = self.data_connect() - - ptt_thread = threading.Thread(target=self.ptt_connect, args=[], daemon=True) - ptt_thread.start() - - data_thread = threading.Thread(target=self.data_connect, args=[], daemon=True) - data_thread.start() - - # wait some time - threading.Event().wait(0.5) - - if self.ptt_connected and self.data_connected: - self.log.debug("Rigctl DATA/PTT initialized") - return True - - self.log.error( - "[RIGCTLD] Can't connect!", ip=self.hostname, port=self.port - ) - return False - - def ptt_connect(self): - """Connect to rigctld instance""" - while True: - - if not self.ptt_connected: - try: - self.ptt_connection = socket.create_connection((self.hostname, self.port)) - self.ptt_connected = True - self.log.info( - "[RIGCTLD] Connected PTT instance to rigctld!", ip=self.hostname, port=self.port - ) - except Exception as err: - # ConnectionRefusedError: [Errno 111] Connection refused - self.close_rig() - self.log.warning( - "[RIGCTLD] PTT Reconnect...", - ip=self.hostname, - port=self.port, - e=err, - ) - - threading.Event().wait(0.5) - - def data_connect(self): - """Connect to rigctld instance""" - while True: - if not self.data_connected: - try: - self.data_connection = socket.create_connection((self.hostname, self.port)) - self.data_connected = True - self.log.info( - "[RIGCTLD] Connected DATA instance to rigctld!", ip=self.hostname, port=self.port - ) - except Exception as err: - # ConnectionRefusedError: [Errno 111] Connection refused - self.close_rig() - self.log.warning( - "[RIGCTLD] DATA Reconnect...", - ip=self.hostname, - port=self.port, - e=err, - ) - threading.Event().wait(0.5) - - def close_rig(self): - """ """ - self.ptt_sock.close() - self.data_sock.close() - self.ptt_connected = False - self.data_connected = False - - def send_ptt_command(self, command, expect_answer) -> bytes: - """Send a command to the connected rotctld instance, - and return the return value. - - Args: - command: - - """ - if self.ptt_connected: + def send_command(self, command) -> str: + if self.connected: try: - self.ptt_connection.sendall(command + b"\n") - except Exception: - self.log.warning( - "[RIGCTLD] Command not executed!", - command=command, - ip=self.hostname, - port=self.port, - ) - self.ptt_connected = False - return b"" - - def send_data_command(self, command, expect_answer) -> bytes: - """Send a command to the connected rotctld instance, - and return the return value. - - Args: - command: - - """ - if self.data_connected: - self.data_connection.setblocking(False) - #Allow a little more time for a response from rigctld before generating a timeout, seems to have no ill effects on a well behaving setup and fixes Issue #373 - self.data_connection.settimeout(0.30) - try: - self.data_connection.sendall(command + b"\n") - - - except Exception: - self.log.warning( - "[RIGCTLD] Command not executed!", - command=command, - ip=self.hostname, - port=self.port, - ) - self.data_connected = False - - try: - # recv seems to be blocking so in case of ptt we don't need the response - # maybe this speeds things up and avoids blocking states - recv = True - data = b'' - - while recv: - try: - - data = self.data_connection.recv(4800) - - except socket.timeout: - recv = False - - return data - - # return self.data_connection.recv(64) if expect_answer else True - except Exception: - self.log.warning( - "[RIGCTLD] No command response!", - command=command, - ip=self.hostname, - port=self.port, - ) - self.data_connected = False - return b"" - - def get_status(self): - """ """ - return True if self.data_connected and self.ptt_connected else False - - def get_level(self): - try: - data = self.send_data_command(b"l RF", True) - data = data.split(b"\n") - rf = data[0].decode("utf-8") - if 'RPRT' not in rf: - try: - self.rf = str(rf) - except ValueError: - self.rf = str(rf) - - return self.rf - except Exception: - return self.rf - - def get_strength(self): - try: - data = self.send_data_command(b"l STRENGTH", True) - data = data.split(b"\n") - strength = data[0].decode("utf-8") - if 'RPRT' not in strength: - try: - self.strength = str(strength) - except ValueError: - self.strength = str(strength) - - return self.strength - except Exception: - return self.strength - - def get_alc(self): - try: - data = self.send_data_command(b"l ALC", True) - data = data.split(b"\n") - alc = data[0].decode("utf-8") - if 'RPRT' not in alc: - try: - alc = float(alc) - except ValueError: - self.alc = 0.0 - - return self.alc - except Exception: - return self.alc - - def get_mode(self): - """ """ - try: - data = self.send_data_command(b"m", True) - data = data.split(b"\n") - data = data[0].decode("utf-8") - if 'RPRT' not in data: - try: - data = int(data) - except ValueError: - self.mode = str(data) - - return self.mode - except Exception: - return self.mode - - def get_bandwidth(self): - """ """ - try: - data = self.send_data_command(b"m", True) - data = data.split(b"\n") - data = data[1].decode("utf-8") - - if 'RPRT' not in data and data not in ['']: - with contextlib.suppress(ValueError): - self.bandwidth = int(data) - return self.bandwidth - except Exception: - return self.bandwidth - - def get_frequency(self): - """ """ - try: - data = self.send_data_command(b"f", True) - data = data.decode("utf-8") - if 'RPRT' not in data and data not in [0, '0', '']: - with contextlib.suppress(ValueError): - data = int(data) - # make sure we have a frequency and not bandwidth - if data >= 10000: - self.frequency = data - return self.frequency - except Exception: - return self.frequency - - def get_ptt(self): - """ """ - try: - return self.send_data_command(b"t", True) - except Exception: - return False + self.connection.sendall(command.encode('utf-8') + b"\n") + response = self.connection.recv(1024) + return response.decode('utf-8').strip() + except Exception as err: + self.log.warning(f"[RIGCTLD] Error sending command to rigctld: {err}") + self.connected = False + return "" def set_ptt(self, state): - """ + """Set the PTT (Push-to-Talk) state. Args: - state: + state (bool): True to enable PTT, False to disable. Returns: - + bool: True if the PTT state was set successfully, False otherwise. """ - try: - if state: - self.send_ptt_command(b"T 1", False) - else: - self.send_ptt_command(b"T 0", False) - return state - except Exception: - return False - - def set_frequency(self, frequency): - """ - - Args: - frequency: - - Returns: - - """ - try: - command = bytes(f"F {frequency}", "utf-8") - self.send_data_command(command, False) - except Exception: - return False + if self.connected: + try: + if state: + self.send_command('T 1') # Enable PTT + else: + self.send_command('T 0') # Disable PTT + self.parameters['ptt'] = state # Update PTT state in parameters + return True + except Exception as err: + self.log.warning(f"[RIGCTLD] Error setting PTT state: {err}") + self.connected = False + return False def set_mode(self, mode): - """ + """Set the mode. Args: - mode: + mode (str): The mode to set. Returns: - + bool: True if the mode was set successfully, False otherwise. """ - try: - command = bytes(f"M {mode} {self.bandwidth}", "utf-8") - self.send_data_command(command, False) - except Exception: - return False \ No newline at end of file + if self.connected: + try: + command = f"M {mode}" + self.send_command(command) + self.parameters['mode'] = mode + return True + except Exception as err: + self.log.warning(f"[RIGCTLD] Error setting mode: {err}") + self.connected = False + return False + + def set_frequency(self, frequency): + """Set the frequency. + + Args: + frequency (str): The frequency to set. + + Returns: + bool: True if the frequency was set successfully, False otherwise. + """ + if self.connected: + try: + command = f"F {frequency}" + self.send_command(command) + self.parameters['frequency'] = frequency + return True + except Exception as err: + self.log.warning(f"[RIGCTLD] Error setting frequency: {err}") + self.connected = False + return False + + def set_bandwidth(self, bandwidth): + """Set the bandwidth. + + Args: + bandwidth (str): The bandwidth to set. + + Returns: + bool: True if the bandwidth was set successfully, False otherwise. + """ + if self.connected: + try: + command = f"M {self.parameters['mode']} {bandwidth}" + self.send_command(command) + self.parameters['bandwidth'] = bandwidth + return True + except Exception as err: + self.log.warning(f"[RIGCTLD] Error setting bandwidth: {err}") + self.connected = False + return False + + def set_rf(self, rf): + """Set the RF. + + Args: + rf (str): The RF to set. + + Returns: + bool: True if the RF was set successfully, False otherwise. + """ + if self.connected: + try: + command = f"l RF {rf}" + self.send_command(command) + self.parameters['rf'] = rf + return True + except Exception as err: + self.log.warning(f"[RIGCTLD] Error setting RF: {err}") + self.connected = False + return False + + def get_parameters(self): + if not self.connected: + self.connect() + + if self.connected: + self.parameters['frequency'] = self.send_command('f') + response = self.send_command( + 'm').strip() # Get the mode/bandwidth response and remove leading/trailing spaces + mode, bandwidth = response.split('\n', 1) # Split the response into mode and bandwidth + + self.parameters['mode'] = mode + self.parameters['bandwidth'] = bandwidth + + self.parameters['alc'] = self.send_command('l ALC') + self.parameters['strength'] = self.send_command('l STRENGTH') + self.parameters['rf'] = self.send_command('l RF') + + """Return the latest fetched parameters.""" + return self.parameters diff --git a/modem/rigdummy.py b/modem/rigdummy.py index 1b63834d..dacdbde6 100644 --- a/modem/rigdummy.py +++ b/modem/rigdummy.py @@ -1,13 +1,30 @@ -hamlib_version = 0 - class radio: """ """ def __init__(self): - pass + self.parameters = { + 'frequency': '---', + 'mode': '---', + 'alc': '---', + 'strength': '---', + 'bandwidth': '---', + 'rf': '---', + 'ptt': False # Initial PTT state is set to False + } - def open_rig(self, **kwargs): + def connect(self, **kwargs): + """ + + Args: + **kwargs: + + Returns: + + """ + return True + + def disconnect(self, **kwargs): """ Args: @@ -98,3 +115,7 @@ class radio: def close_rig(self): """ """ return + + + def get_parameters(self): + return self.parameters \ No newline at end of file diff --git a/modem/server.py b/modem/server.py index 4c532c86..35b18926 100644 --- a/modem/server.py +++ b/modem/server.py @@ -18,6 +18,7 @@ import command_feq import command_test import command_arq_raw import event_manager +import radio_manager app = Flask(__name__) CORS(app) @@ -58,6 +59,7 @@ app.state_manager = state_manager.StateManager(app.state_queue) # start service manager app.service_manager = service_manager.SM(app) + # start modem service app.modem_service.put("start") diff --git a/modem/service_manager.py b/modem/service_manager.py index cdb9a5ec..8bfd6f7e 100644 --- a/modem/service_manager.py +++ b/modem/service_manager.py @@ -6,6 +6,7 @@ import audio import ujson as json import explorer import beacon +import radio_manager class SM: @@ -15,11 +16,12 @@ class SM: self.modem = False self.beacon = False self.explorer = False + self.radio = False self.app = app self.config = self.app.config_manager.read() self.modem_fft = app.modem_fft self.modem_service = app.modem_service - self.states = app.state_manager + self.state_manager = app.state_manager self.event_manager = app.event_manager @@ -34,22 +36,31 @@ class SM: if cmd in ['start'] and not self.modem: self.log.info("------------------ FreeDATA ------------------") self.log.info("------------------ MODEM ------------------") + self.config = self.app.config_manager.read() + self.start_radio_manager() self.start_modem() self.start_explorer_publishing() elif cmd in ['stop'] and self.modem: self.stop_modem() self.stop_explorer_publishing() + self.stop_radio_manager() # we need to wait a bit for avoiding a portaudio crash threading.Event().wait(0.5) elif cmd in ['restart']: self.stop_modem() self.stop_explorer_publishing() + self.stop_radio_manager() # we need to wait a bit for avoiding a portaudio crash threading.Event().wait(0.5) + + self.config = self.app.config_manager.read() + self.start_radio_manager() + if self.start_modem(): self.event_manager.modem_restarted() + self.start_explorer_publishing() elif cmd in ['start_beacon']: self.start_beacon() @@ -59,37 +70,34 @@ class SM: else: - self.log.warning("[SVC] modem command processing failed", cmd=cmd, state=self.states.is_modem_running) + self.log.warning("[SVC] modem command processing failed", cmd=cmd, state=self.state_manager.is_modem_running) def start_modem(self): - # read config - self.config = self.app.config_manager.read() - - if self.states.is_modem_running: + if self.state_manager.is_modem_running: self.log.warning("modem already running") return False # test audio devices audio_test = self.test_audio() - if False in audio_test or None in audio_test or self.states.is_modem_running: + if False in audio_test or None in audio_test or self.state_manager.is_modem_running: self.log.warning("starting modem failed", input_test=audio_test[0], output_test=audio_test[1]) - self.states.set("is_modem_running", False) + self.state_manager.set("is_modem_running", False) self.event_manager.modem_failed() return False self.log.info("starting modem....") - self.modem = modem.RF(self.config, self.event_manager, self.modem_fft, self.modem_service, self.states) + self.modem = modem.RF(self.config, self.event_manager, self.modem_fft, self.modem_service, self.state_manager, self.radio) self.frame_dispatcher = frame_dispatcher.DISPATCHER(self.config, self.event_manager, - self.states, + self.state_manager, self.modem) self.frame_dispatcher.start() self.event_manager.modem_started() - self.states.set("is_modem_running", True) + self.state_manager.set("is_modem_running", True) self.modem.start_modem() return True @@ -98,7 +106,7 @@ class SM: self.log.info("stopping modem....") del self.modem self.modem = False - self.states.set("is_modem_running", False) + self.state_manager.set("is_modem_running", False) self.event_manager.modem_stopped() def test_audio(self): @@ -113,7 +121,7 @@ class SM: return [False, False] def start_beacon(self): - self.beacon = beacon.Beacon(self.config, self.states, self.event_manager, self.log, self.modem) + self.beacon = beacon.Beacon(self.config, self.state_manager, self.event_manager, self.log, self.modem) self.beacon.start() def stop_beacon(self): @@ -123,10 +131,17 @@ class SM: try: # optionally start explorer module if self.config['STATION']['enable_explorer']: - self.explorer = explorer.explorer(self.app, self.config, self.states) + self.explorer = explorer.explorer(self.app, self.config, self.state_manager) except Exception as e: self.log.warning("[EXPLORER] Publishing not started because of error", e=e) def stop_explorer_publishing(self): if self.config['STATION']['enable_explorer']: - del self.explorer \ No newline at end of file + del self.explorer + + def start_radio_manager(self): + self.radio = radio_manager.RadioManager(self.config, self.state_manager, self.event_manager) + + def stop_radio_manager(self): + self.radio.stop() + del self.radio \ No newline at end of file diff --git a/tests/test_protocols.py b/tests/test_protocols.py index 3aedb47e..6b820225 100755 --- a/tests/test_protocols.py +++ b/tests/test_protocols.py @@ -12,7 +12,7 @@ from command_ping import PingCommand from command_cq import CQCommand import modem import frame_handler - +from radio_manager import RadioManager class TestProtocols(unittest.TestCase): @@ -27,9 +27,10 @@ class TestProtocols(unittest.TestCase): cls.event_queue = queue.Queue() cls.event_manager = EventManager([cls.event_queue]) + cls.radio_manager = RadioManager(cls.config, cls.state_manager, cls.event_manager) cls.modem_transmit_queue = queue.Queue() - cls.modem = modem.RF(cls.config, cls.event_queue, queue.Queue(), queue.Queue(), cls.state_manager) + cls.modem = modem.RF(cls.config, cls.event_queue, queue.Queue(), queue.Queue(), cls.state_manager, cls.radio_manager) modem.TESTMODE = True frame_handler.TESTMODE = True