diff --git a/freedata_server/config.py b/freedata_server/config.py index eb91ee47..482d4d59 100644 --- a/freedata_server/config.py +++ b/freedata_server/config.py @@ -90,13 +90,22 @@ class CONFIG: } def __init__(self, configfile: str): + """Initializes a new CONFIG instance. + + This method initializes the configuration handler with the specified + config file. It sets up the logger, config parser, and validates + the config file's existence and structure. + + Args: + configfile (str): The path to the configuration file. + """ # set up logger self.log = structlog.get_logger(type(self).__name__) # init configparser self.parser = configparser.ConfigParser(inline_comment_prefixes="#", allow_no_value=True) - + try: self.config_name = configfile except Exception: @@ -109,10 +118,16 @@ class CONFIG: # validate config structure self.validate_config() - + def config_exists(self): - """ - check if config file exists + """Checks if the configuration file exists and can be read. + + This method attempts to read the configuration file and returns True + if successful, False otherwise. It logs any errors encountered during + the reading process. + + Returns: + bool: True if the config file exists and is readable, False otherwise. """ try: return bool(self.parser.read(self.config_name, None)) @@ -122,6 +137,18 @@ class CONFIG: # Validates config data def validate_data(self, data): + """Validates the data types of configuration settings. + + This method checks if the provided data matches the expected data + types defined in config_types. It raises a ValueError if a data type + mismatch is found. + + Args: + data (dict): The configuration data to validate. + + Raises: + ValueError: If a data type mismatch is found. + """ for section in data: for setting in data[section]: if not isinstance(data[section][setting], self.config_types[section][setting]): @@ -130,9 +157,16 @@ class CONFIG: raise ValueError(message) def validate_config(self): - """ - Updates the configuration file to match exactly what is defined in self.config_types. - It removes sections and settings not defined there and adds missing sections and settings. + """Validates and updates the configuration file structure. + + This method checks the existing configuration file against the + defined config_types. It removes any undefined sections or settings, + adds missing sections and settings with default values, and then + writes the updated configuration back to the file. + + Returns: + dict or bool: A dictionary containing the updated configuration + data if successful, False otherwise. """ existing_sections = self.parser.sections() @@ -162,10 +196,24 @@ class CONFIG: return self.write_to_file() - # Handle special setting data type conversion - # is_writing means data from a dict being writen to the config file - # if False, it means the opposite direction def handle_setting(self, section, setting, value, is_writing=False): + """Handles special data type conversions for config settings. + + This method performs data type conversions for specific config settings, + such as lists and booleans, when reading from or writing to the config + file. It also handles KeyErrors if a setting is not found in the + config_types dictionary. + + Args: + section (str): The config section name. + setting (str): The config setting name. + value: The value to be converted. + is_writing (bool, optional): True if writing to the config file, + False if reading from it. Defaults to False. + + Returns: + The converted value, or the original value if no conversion is needed. + """ try: if self.config_types[section][setting] == list: if is_writing: @@ -192,6 +240,20 @@ class CONFIG: # Sets and writes config data from a dict containing data settings def write(self, data): + """Writes the provided data to the configuration file. + + This method validates the input data, converts it to the appropriate + types, writes it to the configuration file, and returns the updated + configuration as a dictionary. It logs any errors encountered + during the writing process. + + Args: + data (dict): A dictionary containing the configuration data to write. + + Returns: + dict or bool: A dictionary containing the updated configuration + data if successful, False otherwise. + """ # Validate config data before writing print(data) self.validate_data(data) @@ -210,7 +272,17 @@ class CONFIG: return self.write_to_file() def write_to_file(self): - # Write config data to file + """Writes the current configuration to the config file. + + This method writes the in-memory configuration data to the + configuration file. It then rereads and returns the updated + configuration. It logs any errors encountered during the writing + process. + + Returns: + dict or bool: A dictionary containing the updated configuration + data if successful, False otherwise. + """ try: with open(self.config_name, 'w') as configfile: self.parser.write(configfile) @@ -220,8 +292,15 @@ class CONFIG: return False def read(self): - """ - read config file + """Reads the configuration file. + + This method reads the configuration file, handles special setting data + type conversions, and returns the configuration as a dictionary. + It logs any errors encountered during the reading process. + + Returns: + dict or bool: A dictionary containing the configuration data if + successful, False otherwise. """ # self.log.info("[CFG] reading...") if not self.config_exists(): diff --git a/freedata_server/cw.py b/freedata_server/cw.py index 993e5513..852cd9b8 100644 --- a/freedata_server/cw.py +++ b/freedata_server/cw.py @@ -9,7 +9,22 @@ import numpy as np class MorseCodePlayer: + """Generates and plays morse code audio. + + This class provides functionality to convert text to morse code and then + to an audio signal, allowing for the generation and playback of morse + code. It supports customization of the code speed (WPM), tone frequency, + and sampling rate. + """ + def __init__(self, wpm=25, f=1500, fs=48000): + """Initializes the MorseCodePlayer. + + Args: + wpm (int, optional): Words per minute, defining the speed of the morse code. Defaults to 25. + f (int, optional): Tone frequency in Hz. Defaults to 1500. + fs (int, optional): Sampling rate in Hz. Defaults to 48000. + """ self.wpm = wpm self.f0 = f self.fs = fs @@ -29,6 +44,18 @@ class MorseCodePlayer: } def text_to_morse(self, text): + """Converts text to morse code. + + This method takes a string of text as input and converts it to morse + code, using the defined morse alphabet. It handles spaces and + non-alphanumeric characters. + + Args: + text (str): The text to convert. + + Returns: + str: The morse code representation of the input text. + """ morse = '' for char in text: if char.upper() in self.morse_alphabet: @@ -38,6 +65,19 @@ class MorseCodePlayer: return morse def morse_to_signal(self, morse): + """Converts morse code to an audio signal. + + This method takes a string of morse code as input and generates a + corresponding audio signal. Dots and dashes are represented by sine + waves of appropriate durations, and pauses are represented by + silence. + + Args: + morse (str): The morse code string to convert. + + Returns: + numpy.ndarray: The generated audio signal. + """ signal = np.array([], dtype=np.int16) for char in morse: if char == '.': @@ -65,6 +105,18 @@ class MorseCodePlayer: return signal def text_to_signal(self, text): + """Converts text to a morse code audio signal. + + This method takes text as input, converts it to morse code using the + `text_to_morse` method, and then converts the morse code to an audio + signal using the `morse_to_signal` method. + + Args: + text (str): The text to convert to an audio signal. + + Returns: + numpy.ndarray: The generated audio signal as a NumPy array. + """ morse = self.text_to_morse(text) return self.morse_to_signal(morse) diff --git a/freedata_server/state_manager.py b/freedata_server/state_manager.py index 5c05bfec..2f4eb262 100644 --- a/freedata_server/state_manager.py +++ b/freedata_server/state_manager.py @@ -2,6 +2,14 @@ import time import threading import numpy as np class StateManager: + """Manages and updates the state of the FreeDATA server. + + This class stores and manages various state variables related to the + FreeDATA server, including modem status, channel activity, ARQ sessions, + radio parameters, and P2P connections. It provides methods to update + and retrieve state information, as well as manage events and + synchronization. + """ def __init__(self, statequeue): # state related settings @@ -55,14 +63,40 @@ class StateManager: self.radio_status = False def sendState(self): + """Sends the current state to the state queue. + + This method retrieves the current state using get_state_event() and + puts it into the state queue for processing and distribution. + + Returns: + dict: The current state. + """ currentState = self.get_state_event(False) self.statequeue.put(currentState) return currentState def sendStateUpdate(self, state): + """Sends a state update to the state queue. + + This method puts the given state update into the state queue for + processing and distribution. + + Args: + state (dict): The state update to send. + """ self.statequeue.put(state) def set(self, key, value): + """Sets a state variable and sends an update if the state changes. + + This method sets the specified state variable to the given value. + If the new state is different from the current state, it generates + a state update event and sends it to the state queue. + + Args: + key (str): The name of the state variable. + value: The new value for the state variable. + """ setattr(self, key, value) #print(f"State ==> Setting {key} to value {value}") # only process data if changed @@ -72,6 +106,16 @@ class StateManager: self.sendStateUpdate(new_state) def set_radio(self, key, value): + """Sets a radio parameter and sends an update if the value changes. + + This method sets the specified radio parameter to the given value. + If the new value is different from the current value, it generates + a radio update event and sends it to the state queue. + + Args: + key (str): The name of the radio parameter. + value: The new value for the radio parameter. + """ setattr(self, key, value) #print(f"State ==> Setting {key} to value {value}") # only process data if changed @@ -81,6 +125,15 @@ class StateManager: self.sendStateUpdate(new_radio) def set_channel_slot_busy(self, array): + """Sets the channel busy status for each slot. + + This method updates the channel busy status for each slot based on + the provided array. If any slot's status changes, it generates a + state update event and sends it to the state queue. + + Args: + array (list): A list of booleans representing the busy status of each slot. + """ for i in range(0,len(array),1): if not array[i] == self.channel_busy_slot[i]: self.channel_busy_slot = array @@ -89,6 +142,20 @@ class StateManager: continue def get_state_event(self, isChangedState): + """Generates a state event dictionary. + + This method creates a dictionary containing the current state + information, including modem and beacon status, channel activity, + radio status, and audio levels. The type of event ('state' or + 'state-change') is determined by the isChangedState flag. + + Args: + isChangedState (bool): True if the event represents a state change, + False if it's a full state update. + + Returns: + dict: A dictionary containing the state information. + """ msgtype = "state-change" if (not isChangedState): msgtype = "state" @@ -107,6 +174,20 @@ class StateManager: } def get_radio_event(self, isChangedState): + """Generates a radio event dictionary. + + This method creates a dictionary containing the current radio state + information, including frequency, mode, RF level, S-meter strength, + SWR, and tuner status. The type of event ('radio' or 'radio-change') + is determined by the isChangedState flag. + + Args: + isChangedState (bool): True if this is a radio change event, + False for a full radio state update. + + Returns: + dict: A dictionary containing the radio state information. + """ msgtype = "radio-change" if (not isChangedState): msgtype = "radio" diff --git a/freedata_server/websocket_manager.py b/freedata_server/websocket_manager.py index 413c8e81..de8898a9 100644 --- a/freedata_server/websocket_manager.py +++ b/freedata_server/websocket_manager.py @@ -5,7 +5,19 @@ import structlog class wsm: + """Manages WebSocket connections and data transmission. + + This class handles WebSocket connections from clients, manages client + lists for different data types (events, FFT, states), and transmits + data to connected clients via worker threads. It ensures a clean + shutdown of WebSocket connections and related resources. + """ def __init__(self): + """Initializes the WebSocket manager. + + This method sets up the logger, shutdown flag, client lists for + different data types, and worker threads. + """ self.log = structlog.get_logger("WEBSOCKET_MANAGER") self.shutdown_flag = threading.Event() @@ -19,6 +31,17 @@ class wsm: self.fft_thread = None async def handle_connection(self, websocket, client_list, event_queue): + """Handles a WebSocket connection. + + This method adds the new client to the provided list and continuously + listens for incoming messages. If a client disconnects, it removes + the client from the list and logs the event. + + Args: + websocket: The WebSocket object representing the client connection. + client_list (set): The set of connected WebSocket clients. + event_queue (queue.Queue): The event queue. Currently unused. + """ client_list.add(websocket) while not self.shutdown_flag.is_set(): try: @@ -32,6 +55,16 @@ class wsm: break def transmit_sock_data_worker(self, client_list, event_queue): + """Worker thread function for transmitting data to WebSocket clients. + + This method continuously retrieves events from the provided queue and + sends them as JSON strings to all connected clients in the specified + list. It handles client disconnections gracefully. + + Args: + client_list (set): The set of connected WebSocket clients. + event_queue (queue.Queue): The queue containing events to be transmitted. + """ while not self.shutdown_flag.is_set(): try: event = event_queue.get(timeout=1) @@ -50,6 +83,16 @@ class wsm: def startWorkerThreads(self, app): + """Starts worker threads for handling WebSocket data transmission. + + This method creates and starts daemon threads for transmitting modem + events, state updates, and FFT data to connected WebSocket clients. + Each thread uses the transmit_sock_data_worker method to send data + from the respective queues to the appropriate client lists. + + Args: + app: The main application object containing the event queues and client lists. + """ self.events_thread = threading.Thread(target=self.transmit_sock_data_worker, daemon=True, args=(self.events_client_list, app.modem_events)) self.events_thread.start() @@ -60,6 +103,12 @@ class wsm: self.fft_thread.start() def shutdown(self): + """Shuts down the WebSocket manager. + + This method sets the shutdown flag, waits for worker threads to + finish, and logs the shutdown process. It ensures a clean shutdown + of WebSocket connections and related threads. + """ self.log.warning("[SHUTDOWN] closing websockets...") self.shutdown_flag.set() self.events_thread.join(0.5)