more docstrings

pull/912/head
DJ2LS 2025-03-12 13:07:51 +01:00
parent e6b3e83790
commit f40c021a43
11 changed files with 721 additions and 47 deletions

View File

@ -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})

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"],

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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