mirror of https://github.com/DJ2LS/FreeDATA.git
596 lines
22 KiB
Python
596 lines
22 KiB
Python
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
|
|
self.statequeue = statequeue
|
|
self.newstate = None
|
|
self.newradio = None
|
|
self.last = time.time()
|
|
|
|
# freedata_server related states
|
|
# not every state is needed to publish, yet
|
|
# TODO can we reduce them?
|
|
self.channel_busy_slot = [False, False, False, False, False]
|
|
self.channel_busy_event = threading.Event()
|
|
self.channel_busy_condition_traffic = threading.Event()
|
|
self.channel_busy_condition_codec2 = threading.Event()
|
|
|
|
self.is_modem_running = False
|
|
|
|
self.is_modem_busy = threading.Event()
|
|
self.setARQ(False)
|
|
|
|
self.is_beacon_running = False
|
|
self.is_away_from_key = False
|
|
|
|
# If true, any wait() call is blocking
|
|
self.transmitting_event = threading.Event()
|
|
self.setTransmitting(False)
|
|
|
|
self.audio_dbfs = 0
|
|
self.dxcallsign: bytes = b"ZZ9YY-0"
|
|
self.dxgrid: bytes = b"------"
|
|
|
|
self.heard_stations = []
|
|
self.activities_list = {}
|
|
|
|
self.arq_iss_sessions = {}
|
|
self.arq_irs_sessions = {}
|
|
|
|
self.p2p_connection_sessions = {}
|
|
|
|
#self.mesh_routing_table = []
|
|
|
|
self.radio_frequency = 0
|
|
self.radio_mode = None
|
|
self.radio_bandwidth = 0
|
|
self.radio_rf_level = 0
|
|
self.s_meter_strength = 0
|
|
self.radio_tuner = False
|
|
self.radio_swr = 0
|
|
# Set rig control status regardless or rig control method
|
|
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
|
|
new_state = self.get_state_event(True)
|
|
if new_state != self.newstate:
|
|
self.newstate = new_state
|
|
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
|
|
new_radio = self.get_radio_event(True)
|
|
if new_radio != self.newradio:
|
|
self.newradio = new_radio
|
|
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
|
|
self.newstate = self.get_state_event(True)
|
|
self.sendStateUpdate(self.newstate)
|
|
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"
|
|
|
|
return {
|
|
"type": msgtype,
|
|
"is_modem_running": self.is_modem_running,
|
|
"is_beacon_running": self.is_beacon_running,
|
|
"is_away_from_key": self.is_away_from_key,
|
|
"radio_status": self.radio_status,
|
|
"channel_busy_slot": self.channel_busy_slot,
|
|
"is_codec2_traffic": self.is_receiving_codec2_signal(),
|
|
"audio_dbfs": self.audio_dbfs,
|
|
"activities": self.activities_list,
|
|
"is_modem_busy" : self.getARQ()
|
|
}
|
|
|
|
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"
|
|
|
|
return {
|
|
"type": msgtype,
|
|
"radio_status": self.radio_status,
|
|
"radio_frequency": self.radio_frequency,
|
|
"radio_rf_level": self.radio_rf_level,
|
|
"radio_mode": self.radio_mode,
|
|
"s_meter_strength": self.s_meter_strength,
|
|
"radio_swr" : self.radio_swr,
|
|
"radio_tuner": self.radio_tuner,
|
|
}
|
|
|
|
# .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:
|
|
# do a session cleanup of outdated sessions before
|
|
if sessions[session_id].is_session_outdated():
|
|
print(f"session cleanup.....{session_id}")
|
|
if irs:
|
|
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
|
|
|
|
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
|
|
pass
|
|
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
|
|
pass
|
|
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 stop_transmission(self):
|
|
"""Stops any ongoing ARQ transmissions.
|
|
|
|
This method iterates through all active ARQ Information Sending Station (ISS)
|
|
and Information Receiving Station (IRS) sessions and aborts their transmissions.
|
|
ISS sessions are removed after being stopped, while IRS sessions are retained
|
|
to allow for potential resumption.
|
|
"""
|
|
# Stop and remove IRS sessions
|
|
for session_id in list(self.arq_irs_sessions.keys()):
|
|
session = self.arq_irs_sessions[session_id]
|
|
session.transmission_aborted()
|
|
# For now, we don't remove IRS session because of resuming transmissions.
|
|
#self.remove_arq_irs_session(session_id)
|
|
|
|
# Stop and remove ISS sessions
|
|
for session_id in list(self.arq_iss_sessions.keys()):
|
|
session = self.arq_iss_sessions[session_id]
|
|
session.abort_transmission(send_stop=False)
|
|
session.transmission_aborted()
|
|
self.remove_arq_iss_session(session_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()
|
|
|
|
# if timestamp not provided, add it here
|
|
if 'timestamp' not in activity_data:
|
|
activity_data['timestamp'] = int(time.time())
|
|
|
|
# if frequency not provided, add it here
|
|
if 'frequency' not in activity_data:
|
|
activity_data['frequency'] = self.radio_frequency
|
|
self.activities_list[activity_id] = activity_data
|
|
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:
|
|
self.channel_busy_condition_traffic = threading.Event()
|
|
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:
|
|
self.channel_busy_condition_codec2 = threading.Event()
|
|
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,
|
|
"radio_mode": self.radio_mode,
|
|
"radio_rf_level": self.radio_rf_level,
|
|
"s_meter_strength": self.s_meter_strength,
|
|
"radio_swr": self.radio_swr,
|
|
"radio_tuner": self.radio_tuner,
|
|
}
|
|
|
|
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...", session.session_id)
|
|
return False
|
|
self.p2p_connection_sessions[session.session_id] = session
|
|
return True
|
|
|
|
def get_p2p_connection_session(self, id):
|
|
if id not in self.p2p_connection_sessions:
|
|
pass
|
|
return self.p2p_connection_sessions[id]
|
|
|
|
def get_dxcall_by_session_id(self, session_id):
|
|
"""
|
|
Retrieves the dxcall associated with a given session ID by checking both ISS and IRS sessions.
|
|
|
|
Args:
|
|
session_id (str): The ID of the session.
|
|
|
|
Returns:
|
|
str: The dxcall associated with the session ID, or None if not found.
|
|
"""
|
|
try:
|
|
# Check ISS sessions
|
|
if session_id in self.arq_iss_sessions:
|
|
return self.arq_iss_sessions[session_id].dxcall
|
|
|
|
# Check IRS sessions
|
|
if session_id in self.arq_irs_sessions:
|
|
return self.arq_irs_sessions[session_id].dxcall
|
|
#Part of the code to solve P2P connections with the ability to respond to SSIDs other than the one set on the modem.
|
|
#I will circle back around and check that this is really the best way to handle this edge case...
|
|
if session_id in self.p2p_connection_sessions:
|
|
return self.p2p_connection_sessions[session_id].destination
|
|
|
|
# If not found in either session dictionary
|
|
print(f"Session ID {session_id} not found in ISS or IRS sessions")
|
|
return None
|
|
except KeyError:
|
|
print(f"Error retrieving session ID {session_id}")
|
|
return None
|