FreeDATA/freedata_server/rigctld.py

716 lines
26 KiB
Python

import socket
import structlog
import helpers
import threading
class radio:
"""Controls a radio using rigctld.
This class provides methods to interact with a radio using the
rigctld server. It supports connecting, disconnecting, setting
parameters (PTT, mode, frequency, bandwidth, RF level, tuner),
retrieving radio parameters, starting and stopping the rigctld
service, and handling VFO settings.
"""
log = structlog.get_logger("radio (rigctld)")
def __init__(self, ctx):
"""Initializes the radio controller.
Args:
config (dict): Configuration dictionary.
states (StateManager): State manager instance.
hostname (str, optional): Hostname or IP address of the
rigctld server. Defaults to "localhost".
port (int, optional): Port number of the rigctld server.
Defaults to 4532.
timeout (int, optional): Timeout for socket operations in
seconds. Defaults to 3.
"""
self.ctx = ctx
self.hostname = self.ctx.config_manager.config['RIGCTLD']['ip']
self.port = self.ctx.config_manager.config['RIGCTLD']['port']
self.timeout = 3
self.rigctld_process = None
self.max_connection_attempts = 60
self.restart_interval = 10
self.connection = None
self.connected = False
self.shutdown = False
self.await_response = threading.Event()
self.await_response.set()
self.parameters = {
'frequency': '---',
'mode': '---',
'alc': '---',
'strength': '---',
'bandwidth': '---',
'rf': '---',
'ptt': False, # Initial PTT state is set to False,
'tuner': False,
'swr': '---',
'chk_vfo': False,
'vfo': '---',
}
# start rigctld...
if self.ctx.config_manager.config["RADIO"]["control"] in ["rigctld_bundle"]:
self.stop_service()
self.start_service()
# connect to radio
self.connect()
def connect(self):
if self.shutdown:
return
if self.ctx.config_manager.config["RADIO"]["control"] not in ["rigctld", "rigctld_bundle"]:
return
for attempt in range(1, self.max_connection_attempts + 1):
try:
self.log.info(
f"[RIGCTLD] Connection attempt {attempt}/{self.max_connection_attempts} "
f"to {self.hostname}:{self.port}"
)
self.connection = socket.create_connection(
(self.hostname, self.port), timeout=self.timeout
)
self.connection.settimeout(self.timeout)
# allow rigctld to warm up
threading.Event().wait(2)
# success path
self.connected = True
self.ctx.state_manager.set_radio("radio_status", True)
self.log.info(f"[RIGCTLD] Connected to rigctld at {self.hostname}:{self.port}")
self.check_vfo()
self.get_vfo()
return
except Exception as err:
self.connected = False
self.ctx.state_manager.set_radio("radio_status", False)
self.log.warning(f"[RIGCTLD] Attempt {attempt} failed: {err}")
# after every restart_interval failures, restart service
if attempt % self.restart_interval == 0 and attempt < self.max_connection_attempts and self.ctx.config_manager.config["RADIO"]["control"] in ["rigctld_bundle"]:
self.log.info(
f"[RIGCTLD] Reached {attempt} failures, "
f"restarting rigctld service."
)
try:
self.stop_service()
self.start_service()
except Exception as svc_err:
self.log.error(
f"[RIGCTLD] Failed to restart rigctld service: {svc_err}"
)
# give service a moment to start
threading.Event().wait(5)
else:
# brief pause before next attempt
threading.Event().wait(1)
# if still not connected after all attempts
if not self.connected:
self.log.error(
f"[RIGCTLD] Could not establish connection after "
f"{self.max_connection_attempts} attempts"
)
def disconnect(self):
"""Disconnects from the rigctld server.
This method disconnects from the rigctld server, updates the
radio status, resets radio parameters, and closes the socket
connection.
"""
self.shutdown = True
self.connected = False
if self.connection:
self.connection.close()
del self.connection
self.connection = None
self.ctx.state_manager.set_radio("radio_status", False)
self.parameters = {
'frequency': '---',
'mode': '---',
'alc': '---',
'strength': '---',
'bandwidth': '---',
'rf': '---',
'ptt': False, # Initial PTT state is set to False,
'tuner': False,
'swr': '---',
'vfo': '---'
}
def send_command(self, command):
"""Sends a command to the rigctld server.
This method sends a command to the rigctld server and waits for a
response. It handles potential timeouts and other errors during
communication. It uses a threading.Event to synchronize command
sending and avoid concurrent access to the socket.
Args:
command (str): The command to send to rigctld.
Returns:
str or None: The response from rigctld, or None if an error
occurred, the response contains 'RPRT', or the response
contains 'None'.
Raises:
TimeoutError: If a timeout occurs while waiting for a response.
"""
if self.connected:
# wait if we have another command awaiting its response...
# we need to set a timeout for avoiding a blocking state
if not self.await_response.wait(timeout=1): # Check if timeout occurred
raise TimeoutError(f"[RIGCTLD] Timeout waiting for response from rigctld before sending command: [{command}]")
try:
self.await_response.clear() # Signal that a command is awaiting response
if self.connection:
self.connection.sendall(command.encode('utf-8') + b"\n")
else:
return None
response = self.connection.recv(1024)
self.await_response.set() # Signal that the response has been received
stripped_result = response.decode('utf-8').strip()
if 'RPRT' in stripped_result:
return None
if 'None' in stripped_result:
return None
return stripped_result
except socket.timeout:
self.log.warning(f"[RIGCTLD] Timeout waiting for response from rigctld: [{command}]")
self.connected = False # Set connected to False if timeout occurs
return None # Return None to indicate timeout
except Exception as err:
self.log.warning(f"[RIGCTLD] Error sending command [{command}] to rigctld: {err}")
self.connected = False
return None # Return None to indicate error
finally:
self.await_response.set() # Ensure await_response is always set in case of exceptions
return ""
def insert_vfo(self, command):
"""Inserts the VFO into rigctld commands if supported.
This method modifies rigctld commands to include the VFO
information if VFO support is enabled and the VFO is set. It
takes a command string as input and returns the modified command
string with the VFO inserted.
Args:
command (str): The rigctld command string.
Returns:
str: The modified command string with VFO inserted, or the
original command string if VFO is not supported or not set.
"""
#self.get_vfo()
if self.parameters['chk_vfo'] and self.parameters['vfo'] and self.parameters['vfo'] not in [None, False, 'err', 0]:
return f"{command[:1].strip()} {self.parameters['vfo']} {command[1:].strip()}"
return command
def set_ptt(self, state):
"""Set the PTT (Push-to-Talk) state.
Args:
state (bool): True to enable PTT, False to disable.
Returns:
bool: True if the PTT state was set successfully, False otherwise.
"""
if self.connected:
try:
if state:
command = 'T 1'
else:
command = 'T 0'
command = self.insert_vfo(command)
self.send_command(command)
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 (str): The mode to set.
Returns:
bool: True if the mode was set successfully, False otherwise.
"""
if self.connected:
try:
command = f"M {mode} 0"
command = self.insert_vfo(command)
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}"
command = self.insert_vfo(command)
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}"
command = self.insert_vfo(command)
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_level(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 RFPOWER {rf/100}" #RF RFPOWER --> RFPOWER == IC705
command = self.insert_vfo(command)
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 set_tuner(self, state):
"""Set the TUNER state.
Args:
state (bool): True to enable PTT, False to disable.
Returns:
bool: True if the PTT state was set successfully, False otherwise.
"""
if self.connected:
try:
if state:
command = 'U TUNER 1'
else:
command = 'U TUNER 0'
command = self.insert_vfo(command)
self.send_command(command)
self.parameters['tuner'] = state # Update PTT state in parameters
return True
except Exception as err:
self.log.warning(f"[RIGCTLD] Error setting TUNER state: {err}")
self.connected = False
return False
def get_tuner(self):
"""Set the TUNER state.
Args:
state (bool): True to enable PTT, False to disable.
Returns:
bool: True if the PTT state was set successfully, False otherwise.
"""
if self.connected:
try:
command = self.insert_vfo('u TUNER')
result = self.send_command(command)
state = result not in [None, ''] and int(result) == 1
self.parameters['tuner'] = state
return True
except Exception as err:
self.log.warning(f"[RIGCTLD] Error getting TUNER state: {err}")
self.get_vfo()
return False
def get_parameters(self):
if not self.connected:
self.connect()
if self.connected:
#self.check_vfo()
self.get_vfo()
self.get_frequency()
self.get_mode_bandwidth()
self.get_alc()
self.get_strength()
self.get_rf()
self.get_tuner()
self.get_swr()
return self.parameters
def dump_caps(self):
"""Dumps rigctld capabilities.
This method sends the '\dump_caps' command to rigctld and prints
the response. It is used for debugging and informational
purposes. It handles potential errors during command execution.
"""
try:
vfo_response = self.send_command(r'\dump_caps')
print(vfo_response)
except Exception as e:
self.log.warning(f"Error getting dump_caps: {e}")
def check_vfo(self):
"""Checks for VFO support.
This method checks if the connected radio supports VFO by sending
the '\chk_vfo' command to rigctld. It updates the 'chk_vfo'
parameter accordingly and handles potential errors during the
check.
"""
try:
vfo_response = self.send_command(r'\chk_vfo')
if vfo_response in [1, "1"]:
self.parameters['chk_vfo'] = True
else:
self.parameters['chk_vfo'] = False
except Exception as e:
self.log.warning(f"Error getting chk_vfo: {e}")
self.parameters['chk_vfo'] = False
def get_vfo(self):
"""Gets the current VFO.
This method retrieves the current VFO from the radio using the 'v'
command if VFO support is enabled. It updates the 'vfo' parameter
with the retrieved VFO or sets it to 'currVFO' if no specific VFO
is returned. If VFO support is disabled, it sets 'vfo' to False.
It handles potential errors during VFO retrieval.
"""
try:
if self.parameters['chk_vfo']:
vfo_response = self.send_command('v')
if vfo_response not in [None, 'None', '']:
self.parameters['vfo'] = vfo_response.strip('')
else:
self.parameters['vfo'] = 'currVFO'
else:
self.parameters['vfo'] = False
except Exception as e:
self.log.warning(f"Error getting vfo: {e}")
self.parameters['vfo'] = 'err'
def get_frequency(self):
"""Gets the current frequency from the radio.
This method retrieves the current frequency from the radio using
the 'f' command, with VFO support if enabled. It updates the
'frequency' parameter with the retrieved frequency or sets it to
'err' if an error occurs or no frequency is returned. It handles
potential errors during frequency retrieval.
"""
try:
command = self.insert_vfo('f')
frequency_response = self.send_command(command)
if frequency_response not in [None, '']:
self.parameters['frequency'] = int(frequency_response)
else:
self.parameters['frequency'] = 'err'
except Exception as e:
self.log.warning(f"Error getting frequency: {e}")
self.parameters['frequency'] = 'err'
def get_mode_bandwidth(self):
"""Gets the current mode and bandwidth from the radio.
This method retrieves the current mode and bandwidth from the
radio using the 'm' command, with VFO support if enabled. It
updates the 'mode' and 'bandwidth' parameters accordingly. It
handles potential errors during retrieval, including ValueError
if the response cannot be parsed correctly.
"""
try:
command = self.insert_vfo('m')
response = self.send_command(command)
if response not in [None, '']:
response = response.strip()
mode, bandwidth = response.split('\n', 1)
bandwidth = int(bandwidth)
self.parameters['mode'] = mode
self.parameters['bandwidth'] = bandwidth
else:
self.parameters['mode'] = 'err'
self.parameters['bandwidth'] = 'err'
except ValueError:
self.parameters['mode'] = 'err'
self.parameters['bandwidth'] = 'err'
except Exception as e:
self.log.warning(f"Error getting mode and bandwidth: {e}")
self.parameters['mode'] = 'err'
self.parameters['bandwidth'] = 'err'
def get_alc(self):
"""Gets the ALC (Automatic Level Control) value.
This method retrieves the ALC value from the radio using the 'l
ALC' command, with VFO support if enabled. It updates the 'alc'
parameter with the retrieved value or sets it to 'err' if an
error occurs or no value is returned. It handles potential
errors during ALC retrieval.
"""
try:
command = self.insert_vfo('l ALC')
alc_response = self.send_command(command)
if alc_response not in [False, None, '', 'None', 0]:
self.parameters['alc'] = float(alc_response)
else:
self.parameters['alc'] = 'err'
except Exception as e:
self.log.warning(f"Error getting ALC: {e}")
self.parameters['alc'] = 'err'
self.get_vfo()
def get_strength(self):
"""Gets the signal strength.
This method retrieves the signal strength from the radio using
the 'l STRENGTH' command, with VFO support if enabled. It updates
the 'strength' parameter with the retrieved value or sets it to
'err' if an error occurs or no value is returned. It handles
potential errors during strength retrieval.
"""
try:
command = self.insert_vfo('l STRENGTH')
strength_response = self.send_command(command)
if strength_response not in [None, '']:
self.parameters['strength'] = int(strength_response)
else:
self.parameters['strength'] = 'err'
except Exception as e:
self.log.warning(f"Error getting strength: {e}")
self.parameters['strength'] = 'err'
self.get_vfo()
def get_rf(self):
"""Gets the RF power level.
This method retrieves the RF power level from the radio using the
'l RFPOWER' command, with VFO support if enabled. It updates the
'rf' parameter with the retrieved value (converted to integer
percentage) or sets it to 'err' if an error occurs or no value is
returned. It handles potential ValueErrors during conversion and
other exceptions during retrieval.
"""
try:
command = self.insert_vfo('l RFPOWER')
rf_response = self.send_command(command)
if rf_response not in [None, '']:
self.parameters['rf'] = int(float(rf_response) * 100)
else:
self.parameters['rf'] = 'err'
except ValueError:
self.parameters['rf'] = 'err'
except Exception as e:
self.log.warning(f"Error getting RF power: {e}")
self.parameters['rf'] = 'err'
self.get_vfo()
def get_swr(self):
"""Gets the SWR (Standing Wave Ratio) value.
This method retrieves the SWR value from the radio using the 'l
SWR' command, with VFO support if enabled. It updates the 'swr'
parameter with the retrieved value or sets it to 'err' if an
error occurs or no value is returned. It handles potential
ValueErrors and other exceptions during retrieval.
"""
try:
command = self.insert_vfo('l SWR')
rf_response = self.send_command(command)
if rf_response not in [None, '']:
self.parameters['swr'] = rf_response
else:
self.parameters['swr'] = 'err'
except ValueError:
self.parameters['swr'] = 'err'
except Exception as e:
self.log.warning(f"Error getting SWR: {e}")
self.parameters['swr'] = 'err'
self.get_vfo()
def start_service(self):
"""Starts the rigctld service.
This method attempts to start the rigctld service using the
configured parameters and any additional arguments. It searches
for the rigctld binary in common locations, and if found,
attempts to execute it. It handles potential errors during
startup and logs informational and warning messages.
"""
binary_name = "rigctld"
binary_paths = helpers.find_binary_paths(binary_name, search_system_wide=True)
additional_args = self.format_rigctld_args()
print(binary_paths)
if binary_paths:
for binary_path in binary_paths:
try:
self.log.info(f"Attempting to start rigctld using binary found at: {binary_path}")
self.rigctld_process = helpers.kill_and_execute(binary_path, additional_args)
self.log.info(f"Successfully executed rigctld", args=additional_args)
return # Exit the function after successful execution
except Exception as e:
self.log.warning(f"Failed to start rigctld with binary at {binary_path}: {e}") # Log the error
self.log.warning("Failed to start rigctld with all found binaries.", binaries=binary_paths)
else:
self.log.warning("Rigctld binary not found.")
def stop_service(self):
"""Stops the rigctld service.
This method stops the rigctld service if it was previously
started by this class. It uses the helper function
`helpers.kill_process` to terminate the rigctld process.
"""
if self.rigctld_process:
self.log.info("Stopping rigctld service") # Log the action
helpers.kill_process(self.rigctld_process)
def format_rigctld_args(self):
"""Formats the arguments for starting rigctld.
This method reads the configuration and constructs the command-line
arguments for starting the rigctld process. It handles various
settings like model ID, serial port parameters, PTT configuration,
and custom arguments. Values defined as 'ignore', 0, or '0' in the
configuration are skipped. The method returns a list of formatted
arguments.
Returns:
list: A list of strings representing the formatted rigctld arguments.
"""
config = self.ctx.config_manager.config['RADIO']
config_rigctld = self.ctx.config_manager.config['RIGCTLD']
args = []
# Helper function to check if the value should be ignored
def should_ignore(value):
return value in ['ignore', 0]
# Model ID, Serial Port, and Speed
if not should_ignore(config.get('model_id')):
args += ['-m', str(config['model_id'])]
if not should_ignore(config.get('serial_port')):
args += ['-r', config['serial_port']]
if not should_ignore(config.get('serial_speed')):
args += ['-s', str(config['serial_speed'])]
# PTT Port and Type
if not should_ignore(config.get('ptt_port')):
args += ['-p', config['ptt_port']]
if not should_ignore(config.get('ptt_type')):
args += ['-P', config['ptt_type']]
# Serial DCD and DTR
if not should_ignore(config.get('serial_dcd')):
args += ['-D', config['serial_dcd']]
if not should_ignore(config.get('serial_dtr')):
args += ['--set-conf', f'dtr_state={config["serial_dtr"]}']
# Handling Data Bits and Stop Bits
if not should_ignore(config.get('data_bits')):
args += ['--set-conf', f'data_bits={config["data_bits"]}']
if not should_ignore(config.get('stop_bits')):
args += ['--set-conf', f'stop_bits={config["stop_bits"]}']
if self.ctx.config_manager.config['RIGCTLD']['enable_vfo']:
args += ['--vfo']
# Fixme #rts_state
# if not should_ignore(config.get('rts_state')):
# args += ['--set-conf', f'stop_bits={config["rts_state"]}']
# Handle custom arguments for rigctld
# Custom args are split via ' ' so python doesn't add extranaeous quotes on windows
args += config_rigctld["arguments"].split(" ")
print("Hamlib args ==>" + str(args))
return args