From f40c021a4309acbcfde89b8867e098a3b5aa82ee Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:07:51 +0100 Subject: [PATCH] more docstrings --- freedata_server/event_manager.py | 136 ++++++++++++ freedata_server/explorer.py | 24 +++ freedata_server/frame_dispatcher.py | 60 +++++- freedata_server/frame_handler.py | 199 +++++++++++++---- freedata_server/frame_handler_arq_session.py | 14 ++ freedata_server/frame_handler_beacon.py | 12 ++ freedata_server/frame_handler_cq.py | 13 ++ .../frame_handler_p2p_connection.py | 13 ++ freedata_server/frame_handler_ping.py | 23 ++ freedata_server/service_manager.py | 70 ++++++ freedata_server/state_manager.py | 204 +++++++++++++++++- 11 files changed, 721 insertions(+), 47 deletions(-) diff --git a/freedata_server/event_manager.py b/freedata_server/event_manager.py index b941552e..bce15ec5 100644 --- a/freedata_server/event_manager.py +++ b/freedata_server/event_manager.py @@ -3,13 +3,35 @@ import json import structlog class EventManager: + """Manages and broadcasts events within the FreeDATA server. + + This class handles the broadcasting of various events, including PTT + changes, scatter changes, buffer overflows, custom events, ARQ session + updates, and freedata_server status changes, to multiple queues. It + provides a centralized mechanism for distributing event information + throughout the application. + """ def __init__(self, queues): + """Initializes the EventManager with a list of queues. + + Args: + queues (list): A list of queues to which events will be broadcast. + """ self.queues = queues self.logger = structlog.get_logger('Event Manager') self.lastpttstate = False def broadcast(self, data): + """Broadcasts an event to all registered queues. + + This method broadcasts the given event data to all queues registered + with the EventManager. It clears a queue if its size exceeds 10 to + prevent excessive queue buildup. + + Args: + data: The event data to broadcast. + """ for q in self.queues: self.logger.debug(f"Event: ", ev=data) if q.qsize() > 10: @@ -17,21 +39,72 @@ class EventManager: q.put(data) def send_ptt_change(self, on:bool = False): + """Sends a PTT change event. + + This method broadcasts a "ptt" event indicating whether the Push-to-Talk + (PTT) is activated or deactivated. It avoids sending duplicate events + by checking the last PTT state. + + Args: + on (bool, optional): True if PTT is activated, False otherwise. Defaults to False. + """ if (on == self.lastpttstate): return self.lastpttstate= on self.broadcast({"ptt": bool(on)}) def send_scatter_change(self, data): + """Sends a scatter change event. + + This method broadcasts a "scatter" event containing the provided + scatter data as a JSON string. + + Args: + data: The scatter data to send. + """ self.broadcast({"scatter": json.dumps(data)}) def send_buffer_overflow(self, data): + """Sends a buffer overflow event. + + This method broadcasts a "buffer-overflow" event, indicating that a + buffer overflow has occurred. The event data includes the provided + data converted to a string. + + Args: + data: The buffer overflow data to send. + """ self.broadcast({"buffer-overflow": str(data)}) def send_custom_event(self, **event_data): + """Sends a custom event. + + This method broadcasts a custom event with the provided keyword + arguments as the event data. This allows for flexible creation and + distribution of application-specific events. + + Args: + **event_data: Keyword arguments representing the event data. + """ self.broadcast(event_data) def send_arq_session_new(self, outbound: bool, session_id, dxcall, total_bytes, state): + """Sends an event for a new ARQ session. + + This method broadcasts an event indicating the start of a new ARQ + (Automatic Repeat reQuest) session. The event includes information + about the session's direction (inbound or outbound), session ID, + destination callsign, total bytes to be transferred, and initial + state. + + Args: + outbound (bool): True if the session is outbound (sending data), + False if it's inbound (receiving data). + session_id: The unique ID of the ARQ session. + dxcall (str): The callsign of the remote station. + total_bytes (int): The total number of bytes to be transferred. + state (str): The initial state of the ARQ session. + """ direction = 'outbound' if outbound else 'inbound' event = { "type": "arq", @@ -45,6 +118,23 @@ class EventManager: self.broadcast(event) def send_arq_session_progress(self, outbound: bool, session_id, dxcall, received_bytes, total_bytes, state, speed_level, statistics=None): + """Sends an ARQ session progress update event. + + This method broadcasts an event indicating the progress of an ARQ + session. The event includes the session ID, destination callsign, + received and total bytes, current state, speed level, and any + relevant statistics. + + Args: + outbound (bool): True if the session is outbound, False otherwise. + session_id: The ID of the ARQ session. + dxcall (str): The callsign of the remote station. + received_bytes (int): The number of bytes received so far. + total_bytes (int): The total number of bytes to be transferred. + state (str): The current state of the ARQ session. + speed_level: The current speed level of the ARQ session. + statistics (dict, optional): A dictionary containing session statistics. Defaults to None. + """ if statistics is None: statistics = {} @@ -64,6 +154,23 @@ class EventManager: self.broadcast(event) def send_arq_session_finished(self, outbound: bool, session_id, dxcall, success: bool, state: bool, data=False, statistics=None): + """Sends an ARQ session finished event. + + This method broadcasts an event indicating the completion of an ARQ + session. The event includes information about the session's direction, + ID, destination callsign, success status, final state, data + (if any), and statistics. It base64-encodes any data included in + the event. + + Args: + outbound (bool): True if the session was outbound, False otherwise. + session_id: The ID of the ARQ session. + dxcall (str): The callsign of the remote station. + success (bool): True if the session completed successfully, False otherwise. + state (str): The final state of the ARQ session. + data (any, optional): The data transferred during the session. Defaults to False. + statistics (dict, optional): A dictionary of session statistics. Defaults to None. + """ if statistics is None: statistics = {} if data: @@ -86,20 +193,49 @@ class EventManager: self.broadcast(event) def modem_started(self): + """Sends a freedata_server started event. + + This method broadcasts an event indicating that the FreeDATA freedata_server + has started successfully. + """ event = {"freedata_server": "started"} self.broadcast(event) def modem_restarted(self): + """Sends a freedata_server restarted event. + + This method broadcasts an event indicating that the FreeDATA freedata_server + has been restarted. + """ event = {"freedata_server": "restarted"} self.broadcast(event) def modem_stopped(self): + """Sends a freedata_server stopped event. + + This method broadcasts an event indicating that the FreeDATA freedata_server + has stopped. + """ event = {"freedata_server": "stopped"} self.broadcast(event) def modem_failed(self): + """Sends a freedata_server failed event. + + This method broadcasts an event indicating that the FreeDATA freedata_server + has failed to start or has encountered an error. + """ event = {"freedata_server": "failed"} self.broadcast(event) def freedata_message_db_change(self, message_id=None): + """Sends a FreeDATA message database change event. + + This method broadcasts an event indicating that the FreeDATA message + database has been changed. The event includes the ID of the message + that triggered the change, if available. + + Args: + message_id (any, optional): The ID of the changed message. Defaults to None. + """ self.broadcast({"message-db": "changed", "message_id": message_id}) \ No newline at end of file diff --git a/freedata_server/explorer.py b/freedata_server/explorer.py index 992af947..c629bfa1 100644 --- a/freedata_server/explorer.py +++ b/freedata_server/explorer.py @@ -15,7 +15,24 @@ from constants import EXPLORER_API_URL log = structlog.get_logger("explorer") class Explorer: + """Pushes station and last heard data to the FreeDATA explorer. + + This class collects station information, including callsign, gridsquare, + frequency, signal strength, version, bandwidth, beacon status, and + last heard stations, and pushes this data to the FreeDATA explorer API. + """ def __init__(self, modem_version, config_manager, states): + """Initializes the Explorer. + + This method initializes the Explorer with the modem version, + configuration manager, state manager, and the URL of the FreeDATA + explorer API. + + Args: + modem_version (str): The version of the FreeDATA modem. + config_manager (ConfigManager): The configuration manager object. + states (StateManager): The state manager object. + """ self.modem_version = modem_version self.config_manager = config_manager self.config = self.config_manager.read() @@ -23,6 +40,13 @@ class Explorer: self.explorer_url = EXPLORER_API_URL def push(self): + """Pushes station and last heard data to the explorer. + + This method collects station information from the configuration and + state manager, formats it as JSON, and sends it to the FreeDATA + explorer API. It includes error handling and logging for successful + pushes, failed pushes, and connection issues. + """ self.config = self.config_manager.read() frequency = 0 if self.states.radio_frequency is None else self.states.radio_frequency diff --git a/freedata_server/frame_dispatcher.py b/freedata_server/frame_dispatcher.py index bfecdc29..5491d9f1 100644 --- a/freedata_server/frame_dispatcher.py +++ b/freedata_server/frame_dispatcher.py @@ -18,7 +18,14 @@ from frame_handler_beacon import BeaconFrameHandler -class DISPATCHER(): +class DISPATCHER: + """Dispatches received frames to appropriate handlers. + + This class manages the dispatching of received frames to the correct + handler based on the frame type. It initializes frame handlers, starts + worker threads for receiving and processing frames, and provides a + mechanism for stopping the dispatcher. + """ FRAME_HANDLER = { FR_TYPE.ARQ_SESSION_OPEN_ACK.value: {"class": ARQFrameHandler, "name": "ARQ OPEN ACK"}, @@ -52,6 +59,18 @@ class DISPATCHER(): } def __init__(self, config, event_manager, states, modem): + """Initializes the frame dispatcher. + + This method sets up the frame dispatcher with the provided + configuration, event manager, state manager, and modem. It + initializes frame handlers and starts the receive worker thread. + + Args: + config (dict): The configuration dictionary. + event_manager (EventManager): The event manager object. + states (StateManager): The state manager object. + modem: The modem object. + """ self.log = structlog.get_logger("frame_dispatcher") self.log.info("loading frame dispatcher.....\n") @@ -99,6 +118,21 @@ class DISPATCHER(): continue def process_data(self, bytes_out, freedv, bytes_per_frame: int, snr, frequency_offset, mode_name) -> None: + """Processes received data frames. + + This method deconstructs the received data into a frame dictionary, + identifies the frame type, and dispatches the frame to the + appropriate handler based on its type. It logs warnings for + unknown frame types. + + Args: + bytes_out (bytes): The raw frame data. + freedv: The FreeDV instance. + bytes_per_frame (int): The number of bytes per frame. + snr (float): The signal-to-noise ratio of the received frame. + frequency_offset (float): The frequency offset of the received frame. + mode_name (str): The name of the FreeDV mode. + """ # get frame as dictionary deconstructed_frame = self.frame_factory.deconstruct(bytes_out, mode_name=mode_name) frametype = deconstructed_frame["frame_type_int"] @@ -106,17 +140,31 @@ class DISPATCHER(): self.log.warning( "[DISPATCHER] ARQ - other frame type", frametype=FR_TYPE(frametype).name) return - + # instantiate handler handler_class = self.FRAME_HANDLER[frametype]['class'] handler: FrameHandler = handler_class(self.FRAME_HANDLER[frametype]['name'], - self.config, - self.states, - self.event_manager, - self.modem) + self.config, + self.states, + self.event_manager, + self.modem) handler.handle(deconstructed_frame, snr, frequency_offset, freedv, bytes_per_frame) def get_id_from_frame(self, data): + """Extracts the session ID from an ARQ_SESSION_OPEN frame. + + This method checks if the provided data represents an + ARQ_SESSION_OPEN frame and, if so, extracts and returns the session + ID. Otherwise, it returns None. This method is currently not used + in the code. + + Args: + data (bytes): The frame data. + + Returns: + bytes or None: The session ID if the frame is an ARQ_SESSION_OPEN + frame, None otherwise. + """ if data[:1] == FR_TYPE.ARQ_SESSION_OPEN: return data[13:14] return None diff --git a/freedata_server/frame_handler.py b/freedata_server/frame_handler.py index f85421cb..801aa3e1 100644 --- a/freedata_server/frame_handler.py +++ b/freedata_server/frame_handler.py @@ -13,10 +13,25 @@ TESTMODE = False class FrameHandler(): + """Base class for handling received frames. + This class provides common functionality for processing received frames, + including checking if the frame is addressed to the current station, + adding activity to the activity list, managing heard stations, emitting + events, and transmitting responses. Subclasses implement the + `follow_protocol` method to handle specific frame types and protocols. + """ def __init__(self, name: str, config, states: StateManager, event_manager: EventManager, modem) -> None: - + """Initializes a new FrameHandler instance. + + Args: + name (str): The name of the frame handler. + config (dict): The configuration dictionary. + states (StateManager): The state manager object. + event_manager (EventManager): The event manager object. + modem: The modem object. + """ self.name = name self.config = config self.states = states @@ -33,54 +48,85 @@ class FrameHandler(): } def is_frame_for_me(self): - call_with_ssid = self.config['STATION']['mycall'] + "-" + str(self.config['STATION']['myssid']) - ft = self.details['frame']['frame_type'] - valid = False - - # Check for callsign checksum - if ft in ['ARQ_SESSION_OPEN', 'ARQ_SESSION_OPEN_ACK', 'PING', 'PING_ACK', 'P2P_CONNECTION_CONNECT']: - valid, mycallsign = helpers.check_callsign( - call_with_ssid, - self.details["frame"]["destination_crc"], - self.config['STATION']['ssid_list']) + """Checks if the received frame is addressed to this station. - # Check for session id on IRS side - elif ft in ['ARQ_SESSION_INFO', 'ARQ_BURST_FRAME', 'ARQ_STOP']: - session_id = self.details['frame']['session_id'] - if session_id in self.states.arq_irs_sessions: - valid = True + This method checks if the received frame is intended for this + station by verifying the destination callsign CRC and SSID against + the station's configured callsign and SSID list. It also checks for + session IDs in the case of ARQ and P2P frames. - # Check for session id on ISS side - elif ft in ['ARQ_SESSION_INFO_ACK', 'ARQ_BURST_ACK', 'ARQ_STOP_ACK']: - session_id = self.details['frame']['session_id'] - if session_id in self.states.arq_iss_sessions: - valid = True - - # check for p2p connection - elif ft in ['P2P_CONNECTION_CONNECT']: - valid, mycallsign = helpers.check_callsign( - call_with_ssid, - self.details["frame"]["destination_crc"], - self.config['STATION']['ssid_list']) - - #check for p2p connection - elif ft in ['P2P_CONNECTION_CONNECT_ACK', 'P2P_CONNECTION_PAYLOAD', 'P2P_CONNECTION_PAYLOAD_ACK', 'P2P_CONNECTION_DISCONNECT', 'P2P_CONNECTION_DISCONNECT_ACK']: - session_id = self.details['frame']['session_id'] - if session_id in self.states.p2p_connection_sessions: - valid = True - - else: + Returns: + bool: True if the frame is for this station, False otherwise. + """ + call_with_ssid = self.config['STATION']['mycall'] + "-" + str(self.config['STATION']['myssid']) + ft = self.details['frame']['frame_type'] valid = False - if not valid: - self.logger.info(f"[Frame handler] {ft} received but not for us.") + # Check for callsign checksum + if ft in ['ARQ_SESSION_OPEN', 'ARQ_SESSION_OPEN_ACK', 'PING', 'PING_ACK', 'P2P_CONNECTION_CONNECT']: + valid, mycallsign = helpers.check_callsign( + call_with_ssid, + self.details["frame"]["destination_crc"], + self.config['STATION']['ssid_list']) + + # Check for session id on IRS side + elif ft in ['ARQ_SESSION_INFO', 'ARQ_BURST_FRAME', 'ARQ_STOP']: + session_id = self.details['frame']['session_id'] + if session_id in self.states.arq_irs_sessions: + valid = True + + # Check for session id on ISS side + elif ft in ['ARQ_SESSION_INFO_ACK', 'ARQ_BURST_ACK', 'ARQ_STOP_ACK']: + session_id = self.details['frame']['session_id'] + if session_id in self.states.arq_iss_sessions: + valid = True + + # check for p2p connection + elif ft in ['P2P_CONNECTION_CONNECT']: + valid, mycallsign = helpers.check_callsign( + call_with_ssid, + self.details["frame"]["destination_crc"], + self.config['STATION']['ssid_list']) + + # check for p2p connection + elif ft in ['P2P_CONNECTION_CONNECT_ACK', 'P2P_CONNECTION_PAYLOAD', 'P2P_CONNECTION_PAYLOAD_ACK', + 'P2P_CONNECTION_DISCONNECT', 'P2P_CONNECTION_DISCONNECT_ACK']: + session_id = self.details['frame']['session_id'] + if session_id in self.states.p2p_connection_sessions: + valid = True + + else: + valid = False + + if not valid: + self.logger.info(f"[Frame handler] {ft} received but not for us.") + + return valid - return valid def should_respond(self): + """Checks if the frame handler should respond to the received frame. + + This method simply calls is_frame_for_me() to determine if a + response is necessary. It can be overridden by subclasses to + implement more complex response logic. + + Returns: + bool: True if the handler should respond, False otherwise. + """ return self.is_frame_for_me() def is_origin_on_blacklist(self): + """Checks if the origin callsign is on the blacklist. + + This method checks if the origin callsign of the received frame is + present in the callsign blacklist defined in the configuration. + It handles callsigns with SSIDs by removing the suffix and performs + a case-insensitive comparison. + + Returns: + bool: True if the origin callsign is blacklisted, False otherwise. + """ origin_callsign = self.details["frame"]["origin"] # Remove the suffix after the hyphen if it exists @@ -96,6 +142,13 @@ class FrameHandler(): def add_to_activity_list(self): + """Adds the received frame to the activity list. + + This method extracts relevant information from the received frame, + such as origin, destination, gridsquare, SNR, frequency offset, + activity type, session ID, and away-from-key status, and adds it + as a new activity to the state manager's activity list. + """ frame = self.details['frame'] activity = { @@ -123,6 +176,14 @@ class FrameHandler(): self.states.add_activity(activity) def add_to_heard_stations(self): + """Adds the received frame's origin station to the heard stations list. + + This method extracts information from the received frame, including + callsign, gridsquare, signal strength, frequency offset, and + away-from-key status, and adds it to the heard stations list in the + state manager. It also calculates the distance between the current + station and the received station if gridsquares are available. + """ frame = self.details['frame'] if 'origin' not in frame: @@ -157,6 +218,16 @@ class FrameHandler(): away_from_key=away_from_key ) def make_event(self): + """Creates a frame received event dictionary. + + This method constructs a dictionary containing information about the + received frame, including timestamps, callsigns, gridsquares, signal + strength, and distance. This dictionary is used for emitting events + related to frame reception. + + Returns: + dict: A dictionary containing the event data. + """ event = { "type": "frame-handler", @@ -185,26 +256,75 @@ class FrameHandler(): return event def emit_event(self): + """Emits a frame received event. + + This method creates an event dictionary containing information about + the received frame, such as the frame type, timestamp, callsigns, + gridsquare, SNR, distance, and away-from-key status. It then + broadcasts this event through the event manager. + """ event_data = self.make_event() print(event_data) self.event_manager.broadcast(event_data) def get_tx_mode(self): + """Returns the transmission mode for acknowledgements. + + This method returns the FreeDV mode used for transmitting + acknowledgement frames. Currently, it always returns the signalling + mode. + + Returns: + FREEDV_MODE: The FreeDV mode for transmissions. + """ return FREEDV_MODE.signalling def transmit(self, frame): + """Transmits a frame using the modem. + + This method transmits the given frame using the modem. In test mode, + it broadcasts the frame through the event manager instead of using + the modem. + + Args: + frame: The frame to transmit. + """ if not TESTMODE: self.modem.transmit(self.get_tx_mode(), 1, 0, frame) else: self.event_manager.broadcast(frame) def follow_protocol(self): + """Handles protocol-specific actions for the received frame. + + This method is intended to be overridden by subclasses to implement + specific protocol handling logic for different frame types. The base + implementation does nothing. + """ pass def log(self): + """Logs the frame type being handled.""" self.logger.info(f"[Frame Handler] Handling frame {self.details['frame']['frame_type']}") def handle(self, frame, snr, frequency_offset, freedv_inst, bytes_per_frame): + """Handles a received frame. + + This method processes the received frame, updates internal state, + performs blacklist checks, adds the frame to activity lists and heard + stations, emits an event, and calls the follow_protocol method for + subclass-specific handling. + + Args: + frame (dict): The received frame data. + snr (float): The signal-to-noise ratio of the received frame. + frequency_offset (float): The frequency offset of the received frame. + freedv_inst: The FreeDV instance. + bytes_per_frame (int): The number of bytes per frame. + + Returns: + bool: True if the frame was processed successfully, False if it was blocked due to blacklisting. + """ self.details['frame'] = frame self.details['snr'] = snr self.details['frequency_offset'] = frequency_offset @@ -253,3 +373,4 @@ class FrameHandler(): self.add_to_activity_list() self.emit_event() self.follow_protocol() + return True diff --git a/freedata_server/frame_handler_arq_session.py b/freedata_server/frame_handler_arq_session.py index 91b65a44..1d4ba04a 100644 --- a/freedata_server/frame_handler_arq_session.py +++ b/freedata_server/frame_handler_arq_session.py @@ -9,8 +9,22 @@ from arq_session_irs import IRS_State class ARQFrameHandler(frame_handler.FrameHandler): + """Handles ARQ frames and manages ARQ sessions. + + This class processes incoming ARQ frames, manages ARQ sessions for both + ISS (Information Sending Station) and IRS (Information Receiving Station), + and dispatches frames to the appropriate session based on their type + and session ID. + """ def follow_protocol(self): + """Processes the received ARQ frame based on its type. + + This method handles different ARQ frame types, including session + open, information, burst data, stop, and various acknowledgements. + It manages session creation, retrieval, and updates based on the + frame type and session ID. + """ if not self.should_respond(): return diff --git a/freedata_server/frame_handler_beacon.py b/freedata_server/frame_handler_beacon.py index c1ff16c6..1c6b74ae 100644 --- a/freedata_server/frame_handler_beacon.py +++ b/freedata_server/frame_handler_beacon.py @@ -6,8 +6,20 @@ from message_system_db_messages import DatabaseManagerMessages from message_system_db_manager import DatabaseManager class BeaconFrameHandler(frame_handler.FrameHandler): + """Handles received beacon frames. + + This class processes received beacon frames, stores them in the database, + and checks for queued messages to be sent based on configuration and + signal strength. + """ def follow_protocol(self): + """Processes the received beacon frame. + + This method adds the beacon information to the database and checks + for queued messages to send if auto-repeat is enabled and the + signal strength is above a certain threshold. + """ DatabaseManagerBeacon(self.event_manager).add_beacon(datetime.datetime.now(), self.details['frame']["origin"], self.details["snr"], diff --git a/freedata_server/frame_handler_cq.py b/freedata_server/frame_handler_cq.py index 32b4d7f6..e41acb8b 100644 --- a/freedata_server/frame_handler_cq.py +++ b/freedata_server/frame_handler_cq.py @@ -8,12 +8,25 @@ from message_system_db_messages import DatabaseManagerMessages import numpy as np class CQFrameHandler(frame_handler.FrameHandler): + """Handles received CQ frames. + + This class processes received CQ (Calling Any Station) frames and sends + a QRV (Ready to Receive) frame as an acknowledgement if the station is + not currently busy with ARQ. It also checks for queued messages to be + sent based on the configuration. + """ #def should_respond(self): # self.logger.debug(f"Respond to CQ: {self.config['MODEM']['respond_to_cq']}") # return bool(self.config['MODEM']['respond_to_cq'] and not self.states.getARQ()) def follow_protocol(self): + """Processes the received CQ frame. + + This method checks if the modem is currently busy with ARQ. If not, + it sends a QRV frame as an acknowledgement and checks for queued + messages to send. + """ if self.states.getARQ(): return diff --git a/freedata_server/frame_handler_p2p_connection.py b/freedata_server/frame_handler_p2p_connection.py index dc52a131..5204845e 100644 --- a/freedata_server/frame_handler_p2p_connection.py +++ b/freedata_server/frame_handler_p2p_connection.py @@ -6,8 +6,21 @@ from modem_frametypes import FRAME_TYPE as FR from p2p_connection import P2PConnection class P2PConnectionFrameHandler(frame_handler.FrameHandler): + """Handles P2P connection frames. + + This class processes P2P connection frames, manages P2P connections, + and dispatches frames to the appropriate connection based on their + type and session ID. + """ def follow_protocol(self): + """Processes received P2P connection frames. + + This method handles different P2P frame types, including connection + requests, acknowledgements, payload data, disconnections, and payload + acknowledgements. It manages connection creation, retrieval, and + updates based on the frame type and session ID. + """ if not self.should_respond(): return diff --git a/freedata_server/frame_handler_ping.py b/freedata_server/frame_handler_ping.py index f6aed1d4..9a8392a5 100644 --- a/freedata_server/frame_handler_ping.py +++ b/freedata_server/frame_handler_ping.py @@ -5,6 +5,11 @@ from message_system_db_messages import DatabaseManagerMessages class PingFrameHandler(frame_handler.FrameHandler): + """Handles received PING frames. + + This class processes received PING frames, sends acknowledgements, and + checks for queued messages to be sent based on configuration. + """ #def is_frame_for_me(self): # call_with_ssid = self.config['STATION']['mycall'] + "-" + str(self.config['STATION']['myssid']) @@ -19,6 +24,12 @@ class PingFrameHandler(frame_handler.FrameHandler): # return valid def follow_protocol(self): + """Processes the received PING frame. + + This method checks if the frame is for the current station and if + the modem is not busy with ARQ. If both conditions are met, it sends + a PING acknowledgement and checks for queued messages to send. + """ if not bool(self.is_frame_for_me() and not self.states.getARQ()): return self.logger.debug( @@ -31,6 +42,11 @@ class PingFrameHandler(frame_handler.FrameHandler): self.check_for_queued_message() def send_ack(self): + """Sends a PING acknowledgement frame. + + This method builds a PING acknowledgement frame using the received + frame's origin CRC and SNR, and transmits it using the modem. + """ factory = data_frame_factory.DataFrameFactory(self.config) ping_ack_frame = factory.build_ping_ack( self.details['frame']['origin_crc'], @@ -39,6 +55,13 @@ class PingFrameHandler(frame_handler.FrameHandler): self.transmit(ping_ack_frame) def check_for_queued_message(self): + """Checks for queued messages to send. + + This method checks if auto-repeat is enabled in the configuration + and if the received signal strength is above a certain threshold. + If both conditions are met, it sets any messages addressed to the + originating station to 'queued' status in the message database. + """ # only check for queued messages, if we have enabled this and if we have a minimum snr received if self.config["MESSAGES"]["enable_auto_repeat"] and self.details["snr"] >= -2: diff --git a/freedata_server/service_manager.py b/freedata_server/service_manager.py index ed890021..b718efc6 100644 --- a/freedata_server/service_manager.py +++ b/freedata_server/service_manager.py @@ -8,7 +8,24 @@ from socket_interface import SocketInterfaceHandler import queue class SM: + """Manages the FreeDATA server services. + + This class controls the starting, stopping, and restarting of the modem, + radio manager, and socket interface. It handles commands from the modem + service queue and performs actions based on the received commands. + """ def __init__(self, app): + """Initializes the service manager. + + This method sets up the service manager with references to the main + application object and its components, including the config manager, + modem, radio manager, state manager, event manager, and schedule + manager. It also initializes the socket interface manager if enabled + in the configuration and starts the runner thread. + + Args: + app: The main application object. + """ self.log = structlog.get_logger("service manager") self.app = app self.modem = False @@ -32,6 +49,13 @@ class SM: def runner(self): + """Main loop for handling service commands. + + This method continuously monitors the modem service queue for + commands and executes the corresponding actions. It handles starting, + stopping, and restarting the modem, radio manager, and socket + interface. + """ while not self.shutdown_flag.is_set(): try: cmd = self.modem_service.get() @@ -84,6 +108,15 @@ class SM: self.modem_service.queue.clear() def start_modem(self): + """Starts the FreeDATA modem. + + This method initializes and starts the RF modem, frame dispatcher, + and schedule manager. It performs checks for valid callsign and + audio device functionality before starting the modem. + + Returns: + bool: True if the modem started successfully, False otherwise. + """ if self.config['STATION']['mycall'] in ['XX1XXX']: self.log.warning("wrong callsign in config! interrupting startup") @@ -118,6 +151,14 @@ class SM: return True def stop_modem(self): + """Stops the FreeDATA modem and related services. + + This method stops the RF modem, frame dispatcher, and schedule + manager. It also updates the modem running state and emits a + 'modem_stopped' event. It handles potential AttributeErrors that + may occur during the stopping process if components are not + initialized. + """ self.log.warning("stopping modem....") try: if self.modem and hasattr(self.app, 'modem_service'): @@ -141,6 +182,18 @@ class SM: self.event_manager.modem_stopped() def test_audio(self): + """Tests the configured audio devices. + + This method tests the input and output audio devices specified in + the configuration. It logs the test results and returns a list + indicating whether each device passed the test. + + Returns: + list: A list of booleans, where the first element represents the + input device test result and the second element represents the + output device test result. Returns [False, False] if an error + occurs during testing. + """ try: audio_test = audio.test_audio_devices(self.config['AUDIO']['input_device'], self.config['AUDIO']['output_device']) @@ -152,14 +205,31 @@ class SM: return [False, False] def start_radio_manager(self): + """Starts the radio manager. + + This method initializes and starts the RadioManager, which handles + communication with the radio. + """ self.app.radio_manager = radio_manager.RadioManager(self.config, self.state_manager, self.event_manager) def stop_radio_manager(self): + """Stops the radio manager. + + This method stops the RadioManager and releases the associated + resources. It handles potential AttributeErrors if the radio manager + has not been initialized. + """ if hasattr(self.app, 'radio_manager'): self.app.radio_manager.stop() del self.app.radio_manager def shutdown(self): + """Shuts down the service manager. + + This method stops the modem, sets the shutdown flag, and waits for + the runner thread to finish. This ensures a clean shutdown of all + managed services. + """ self.log.warning("[SHUTDOWN] stopping service manager....") self.modem_service.put("stop") threading.Event().wait(2) # we need some time before processing with the shutdown_event_flag diff --git a/freedata_server/state_manager.py b/freedata_server/state_manager.py index 2f4eb262..febc0799 100644 --- a/freedata_server/state_manager.py +++ b/freedata_server/state_manager.py @@ -205,43 +205,134 @@ class StateManager: # .wait() blocks until the event is set def isTransmitting(self): + """Checks if the server is currently transmitting. + + This method returns True if the transmitting_event is not set, + indicating that a transmission is in progress. Otherwise, it + returns False. + + Returns: + bool: True if transmitting, False otherwise. + """ return not self.transmitting_event.is_set() # .wait() blocks until the event is set def setTransmitting(self, transmitting: bool): + """Sets the transmitting status of the server. + + This method controls the transmitting_event, which is used for + synchronization and blocking other operations during transmissions. + If transmitting is True, the event is cleared (set to non-signaled + state), causing any threads waiting on it to block. If transmitting + is False, the event is set (signaled state), allowing waiting + threads to proceed. + + Args: + transmitting (bool): True if the server is transmitting, False otherwise. + """ if transmitting: self.transmitting_event.clear() else: self.transmitting_event.set() def setARQ(self, busy): + """Sets the ARQ status. + + This method sets the is_modem_busy event based on the provided + busy flag. If busy is True, the event is cleared, indicating that + the modem is busy with ARQ. If busy is False, the event is set, + indicating that the modem is available. + + Args: + busy (bool): True if ARQ is busy, False otherwise. + """ if busy: self.is_modem_busy.clear() else: self.is_modem_busy.set() def getARQ(self): + """Gets the ARQ status. + + This method returns True if the is_modem_busy event is not set, + indicating that ARQ is currently not busy. Otherwise, it returns + False. + + Returns: + bool: True if ARQ is not busy, False otherwise. + """ return not self.is_modem_busy.is_set() def waitForTransmission(self): + """Waits for any ongoing transmissions to complete. + + This method blocks the calling thread until the transmitting_event + is set, indicating that the server is no longer transmitting. + """ self.transmitting_event.wait() def waitForChannelBusy(self): + """Waits for the channel busy event. + + This method waits for the channel_busy_event to be set, with a + timeout of 2 seconds. This is used to pause operations when the + channel is detected as busy. + """ self.channel_busy_event.wait(2) def register_arq_iss_session(self, session): + """Registers an ARQ ISS session. + + This method registers an ARQ Information Sending Station (ISS) session + by storing it in the arq_iss_sessions dictionary. It returns True + if the session is successfully registered, False if a session with + the same ID already exists. + + Args: + session (ARQSessionISS): The ARQ ISS session to register. + + Returns: + bool: True if the session was registered, False otherwise. + """ if session.id in self.arq_iss_sessions: return False self.arq_iss_sessions[session.id] = session return True def register_arq_irs_session(self, session): + """Registers an ARQ IRS session. + + This method registers an ARQ Information Receiving Station (IRS) + session, storing it in the arq_irs_sessions dictionary. It + returns True if the session is registered successfully, or False + if a session with the same ID already exists. + + Args: + session (ARQSessionIRS): The ARQ IRS session to register. + + Returns: + bool: True if the session was registered, False otherwise. + """ if session.id in self.arq_irs_sessions: return False self.arq_irs_sessions[session.id] = session return True def check_if_running_arq_session(self, irs=False): + """Checks if there is a running ARQ session. + + This method iterates through either the ISS or IRS ARQ sessions, + depending on the 'irs' flag. It cleans up outdated sessions and + checks if any remaining sessions are not in a final state (ENDED, + ABORTED, or FAILED). + + Args: + irs (bool, optional): If True, checks IRS sessions; otherwise, + checks ISS sessions. Defaults to False (ISS sessions). + + Returns: + bool: True if there is a running ARQ session, False otherwise. + """ sessions = self.arq_irs_sessions if irs else self.arq_iss_sessions for session_id in sessions: @@ -252,15 +343,26 @@ class StateManager: self.remove_arq_irs_session(session_id) else: self.remove_arq_iss_session(session_id) - + # check again if session id exists in session because of cleanup if session_id in sessions and sessions[session_id].state.name not in ['ENDED', 'ABORTED', 'FAILED']: print(f"[State Manager] running session...[{session_id}]") return True - return False return False def get_arq_iss_session(self, id): + """Retrieves an ARQ ISS session by ID. + + This method returns the ARQ Information Sending Station (ISS) session + associated with the given ID. If no session with the given ID is + found, it returns None. + + Args: + id: The ID of the ARQ ISS session. + + Returns: + ARQSessionISS or None: The ARQSessionISS object if found, None otherwise. + """ if id not in self.arq_iss_sessions: #raise RuntimeError(f"ARQ ISS Session '{id}' not found!") # DJ2LS: WIP We need to find a better way of handling this @@ -268,6 +370,18 @@ class StateManager: return self.arq_iss_sessions[id] def get_arq_irs_session(self, id): + """Retrieves an ARQ IRS session by ID. + + This method returns the ARQ Information Receiving Station (IRS) session + associated with the given ID. It returns None if no session with the + given ID is found. + + Args: + id: The ID of the ARQ IRS session. + + Returns: + ARQSessionIRS or None: The ARQSessionIRS object if found, None otherwise. + """ if id not in self.arq_irs_sessions: #raise RuntimeError(f"ARQ IRS Session '{id}' not found!") # DJ2LS: WIP We need to find a better way of handling this @@ -275,14 +389,39 @@ class StateManager: return self.arq_irs_sessions[id] def remove_arq_iss_session(self, id): + """Removes an ARQ ISS session. + + This method removes the ARQ Information Sending Station (ISS) session + associated with the given ID from the arq_iss_sessions dictionary. + + Args: + id: The ID of the ARQ ISS session to remove. + """ if id in self.arq_iss_sessions: del self.arq_iss_sessions[id] def remove_arq_irs_session(self, id): + """Removes an ARQ IRS session. + + This method removes the ARQ Information Receiving Station (IRS) session + associated with the given ID from the arq_irs_sessions dictionary. + + Args: + id: The ID of the ARQ IRS session to remove. + """ if id in self.arq_irs_sessions: del self.arq_irs_sessions[id] def add_activity(self, activity_data): + """Adds a new activity to the activities list. + + This method generates a unique ID for the activity, adds a timestamp + and frequency if not provided, and stores the activity data in the + activities list. It then triggers a state update. + + Args: + activity_data (dict): A dictionary containing the activity data. + """ # Generate a random 8-byte string as hex activity_id = np.random.bytes(8).hex() @@ -297,12 +436,31 @@ class StateManager: self.sendStateUpdate(self.newstate) def calculate_channel_busy_state(self): + """Calculates and sets the overall channel busy state. + + This method determines the channel busy state based on the status + of channel_busy_condition_traffic and + channel_busy_condition_codec2. If both events are set (not busy), + it sets the channel_busy_event, indicating the channel is available. + Otherwise, it resets the channel_busy_event. + """ if self.channel_busy_condition_traffic.is_set() and self.channel_busy_condition_codec2.is_set(): self.channel_busy_event.set() else: self.channel_busy_event = threading.Event() def set_channel_busy_condition_traffic(self, busy): + """Sets the channel busy condition based on data traffic. + + This method sets or clears the channel_busy_condition_traffic event + based on the provided busy flag. If busy is False, the event is set, + indicating no traffic. If busy is True, the event is cleared, + indicating traffic. It then recalculates the overall channel busy + state. + + Args: + busy (bool): True if there is data traffic, False otherwise. + """ if not busy: self.channel_busy_condition_traffic.set() else: @@ -310,6 +468,17 @@ class StateManager: self.calculate_channel_busy_state() def set_channel_busy_condition_codec2(self, traffic): + """Sets the channel busy condition based on Codec2 traffic. + + This method sets or clears the channel_busy_condition_codec2 event + based on the provided traffic flag. If traffic is False, the event + is set, indicating no Codec2 traffic. If traffic is True, the event + is cleared, indicating ongoing Codec2 traffic. It then recalculates + the overall channel busy state. + + Args: + traffic (bool): True if there is Codec2 traffic, False otherwise. + """ if not traffic: self.channel_busy_condition_codec2.set() else: @@ -317,9 +486,27 @@ class StateManager: self.calculate_channel_busy_state() def is_receiving_codec2_signal(self): + """Checks if the server is receiving a Codec2 signal. + + This method returns True if the channel_busy_condition_codec2 event + is not set, indicating that a Codec2 signal is being received. + Otherwise, it returns False. + + Returns: + bool: True if a Codec2 signal is being received, False otherwise. + """ return not self.channel_busy_condition_codec2.is_set() def get_radio_status(self): + """Returns the current radio status. + + This method returns a dictionary containing the current status + information of the radio, including its status, frequency, mode, + RF level, S-meter strength, SWR, and tuner status. + + Returns: + dict: A dictionary containing the radio status information. + """ return { "radio_status": self.radio_status, "radio_frequency": self.radio_frequency, @@ -331,6 +518,19 @@ class StateManager: } def register_p2p_connection_session(self, session): + """Registers a P2P connection session. + + This method registers a peer-to-peer (P2P) connection session by + storing it in the p2p_connection_sessions dictionary. It returns + True if the session is successfully registered, or False if a + session with the same ID already exists. + + Args: + session (P2PConnection): The P2P connection session object. + + Returns: + bool: True if the session was registered, False otherwise. + """ if session.session_id in self.p2p_connection_sessions: print("session already registered...") return False