FreeDATA/freedata_server/arq_session.py

396 lines
15 KiB
Python

import datetime
import threading
import data_frame_factory
import structlog
from event_manager import EventManager
from modem_frametypes import FRAME_TYPE
import time
from arq_data_type_handler import ARQDataTypeHandler
from codec2 import FREEDV_MODE_USED_SLOTS, FREEDV_MODE
import stats
class ARQSession:
"""Manages an ARQ (Automatic Repeat reQuest) session.
This class handles the transmission and reception of data using the ARQ protocol,
managing session state, statistics, and data handling.
"""
SPEED_LEVEL_DICT = {
0: {
'mode': FREEDV_MODE.datac4,
'min_snr': -10,
'duration_per_frame': 5.17,
'bandwidth': 250,
'slots': FREEDV_MODE_USED_SLOTS.datac4,
},
1: {
'mode': FREEDV_MODE.data_ofdm_500,
'min_snr': 0,
'duration_per_frame': 3.19,
'bandwidth': 500,
'slots': FREEDV_MODE_USED_SLOTS.data_ofdm_500,
},
2: {
'mode': FREEDV_MODE.datac1,
'min_snr': 3,
'duration_per_frame': 4.18,
'bandwidth': 1700,
'slots': FREEDV_MODE_USED_SLOTS.datac1,
},
3: {
'mode': FREEDV_MODE.data_ofdm_2438,
'min_snr': 8.5,
'duration_per_frame': 5.5,
'bandwidth': 2438,
'slots': FREEDV_MODE_USED_SLOTS.data_ofdm_2438,
},
# 4: {
# 'mode': FREEDV_MODE.qam16c2,
# 'min_snr': 11,
# 'duration_per_frame': 2.8,
# 'bandwidth': 2438,
# 'slots': FREEDV_MODE_USED_SLOTS.qam16c2,
# },
}
def __init__(self, ctx, dxcall: str):
"""Initializes a new ARQ session.
This method sets up the ARQ session with the provided configuration,
modem, DX call sign, and state manager. It initializes various
session parameters and data structures.
Args:
config (dict): The configuration dictionary.
modem: The modem object.
dxcall (str): The DX call sign.
state_manager: The state manager object.
"""
self.logger = structlog.get_logger(type(self).__name__)
self.ctx = ctx
#self.ctx.state_manager = freedata_server.states
self.ctx.state_manager.setARQ(True)
self.is_IRS = False # state for easy check "is IRS" or is "ISS"
self.protocol_version = self.ctx.constants.ARQ_PROTOCOL_VERSION
self.snr = []
self.dxcall = dxcall
self.dx_snr = []
self.speed_level = 0
self.previous_speed_level = 0
self.frames_per_burst = 1
self.frame_factory = data_frame_factory.DataFrameFactory(self.ctx)
self.event_frame_received = threading.Event()
self.arq_data_type_handler = ARQDataTypeHandler(self.ctx)
self.id = None
self.session_started = time.time()
self.session_ended = 0
self.session_max_age = 500
# this timestamp is updated by "set_state", everytime we have a state change.
# we will use the schedule manager, for checking, how old is the state change for deciding, how we continue with the message
self.last_state_change_timestamp = time.time()
self.statistics = stats.stats(self.ctx)
# histogram lists for storing statistics
self.snr_histogram = []
self.bpm_histogram = []
self.bps_histogram = []
self.time_histogram = []
def log(self, message, isWarning=False):
"""Logs a message with session context.
Logs a message, including the class name, session ID, and current state,
using the appropriate log level (warning or info).
Args:
message: The message to be logged.
isWarning: A boolean indicating whether the message should be logged as a warning.
"""
msg = f"[{type(self).__name__}][id={self.id}][{self.dxcall}][state={self.state}]: {message}"
logger = self.logger.warn if isWarning else self.logger.info
logger(msg)
def get_mode_by_speed_level(self, speed_level):
"""Returns the FreeDV mode for the given speed level.
This method retrieves the FreeDV mode associated with a specific speed level
from the SPEED_LEVEL_DICT.
Args:
speed_level (int): The speed level.
Returns:
modem_frametypes.FREEDV_MODE: The FreeDV mode corresponding to the speed level.
"""
return self.SPEED_LEVEL_DICT[speed_level]["mode"]
def transmit_frame(self, frame: bytearray, mode='auto'):
"""Transmits a given frame using the modem.
This method transmits the provided frame using the configured modem.
If the mode is set to 'auto', it determines the appropriate FreeDV mode
based on the current speed level.
Args:
frame (bytearray): The frame to be transmitted.
mode (str, optional): The FreeDV mode to use for transmission. Defaults to 'auto'.
"""
self.log("Transmitting frame")
if mode in ['auto']:
mode = self.get_mode_by_speed_level(self.speed_level)
self.ctx.rf_modem.transmit(mode, 1, 1, frame)
def set_state(self, state):
"""Sets the state of the ARQ session.
This method updates the session state and logs the state change.
It also updates the timestamp of the last state change.
Args:
state: The new state of the ARQ session.
"""
self.last_state_change_timestamp = time.time()
if self.state == state:
self.log(f"{type(self).__name__} state {self.state.name} unchanged.")
else:
self.log(f"{type(self).__name__} state change from {self.state.name} to {state.name} at {self.last_state_change_timestamp}")
self.state = state
def get_data_payload_size(self):
"""Returns the available data payload size for the current speed level.
This method calculates the available data payload size for ARQ burst frames
based on the current speed level and FreeDV mode.
Returns:
int: The available data payload size in bytes.
"""
return self.frame_factory.get_available_data_payload_for_mode(
FRAME_TYPE.ARQ_BURST_FRAME,
self.SPEED_LEVEL_DICT[self.speed_level]["mode"]
)
def set_details(self, snr, frequency_offset):
"""Sets the SNR and frequency offset for the ARQ session.
This method updates the signal-to-noise ratio (SNR) and frequency offset
values for the current session.
Args:
snr: The signal-to-noise ratio.
frequency_offset: The frequency offset.
"""
self.snr = snr
self.frequency_offset = frequency_offset
def on_frame_received(self, frame):
"""Handles received frames based on the current session state.
This method processes incoming frames, triggering state transitions and
data handling based on the frame type and current session state.
It logs received frame types and ignores unknown state transitions.
Args:
frame: The received frame.
"""
self.event_frame_received.set()
self.log(f"Received {frame['frame_type']}")
frame_type = frame['frame_type_int']
if self.state in self.STATE_TRANSITION and frame_type in self.STATE_TRANSITION[self.state]:
action_name = self.STATE_TRANSITION[self.state][frame_type]
received_data, type_byte = getattr(self, action_name)(frame)
if isinstance(received_data, bytearray) and isinstance(type_byte, int):
self.arq_data_type_handler.dispatch(type_byte, received_data,
self.update_histograms(len(received_data), len(received_data)))
return
self.log(f"Ignoring unknown transition from state {self.state.name} with frame {frame['frame_type']}")
def is_session_outdated(self):
"""Checks if the session is outdated.
This method checks if the session has ended and if the end time is
older than the session_max_age. It returns True if the session is
outdated, False otherwise.
Returns:
bool: True if the session is outdated, False otherwise.
"""
session_alivetime = time.time() - self.session_max_age
return self.session_ended < session_alivetime and self.state.name in [
'FAILED',
'ENDED',
'ABORTED',
]
def calculate_session_duration(self):
"""Calculates the duration of the ARQ session.
This method calculates the duration of the session, taking into account
whether the session has ended or is still ongoing.
Returns:
float: The duration of the session in seconds.
"""
if self.session_ended == 0:
return time.time() - self.session_started
return self.session_ended - self.session_started
def calculate_session_statistics(self, confirmed_bytes, total_bytes):
"""Calculates session statistics.
This method calculates various statistics for the ARQ session, including
duration, bytes per minute, bits per second, and histograms for time,
SNR, BPM, and BPS.
Args:
confirmed_bytes: The number of confirmed bytes transmitted.
total_bytes: The total number of bytes transmitted.
Returns:
dict: A dictionary containing the calculated session statistics.
"""
duration = self.calculate_session_duration()
# total_bytes = self.total_length
# self.total_length
duration_in_minutes = duration / 60 # Convert duration from seconds to minutes
# Calculate bytes per minute
if duration_in_minutes > 0:
bytes_per_minute = int(confirmed_bytes / duration_in_minutes)
else:
bytes_per_minute = 0
# Calculate bits per second
bits_per_second = int((confirmed_bytes * 8) / duration)
# Convert histograms lists to dictionaries
time_histogram_dict = dict(enumerate(self.time_histogram))
snr_histogram_dict = dict(enumerate(self.snr_histogram))
bpm_histogram_dict = dict(enumerate(self.bpm_histogram))
bps_histogram_dict = dict(enumerate(self.bps_histogram))
return {
'total_bytes': total_bytes,
'duration': duration,
'bytes_per_minute': bytes_per_minute,
'bits_per_second': bits_per_second,
'time_histogram': time_histogram_dict,
'snr_histogram': snr_histogram_dict,
'bpm_histogram': bpm_histogram_dict,
'bps_histogram': bps_histogram_dict,
}
def update_histograms(self, confirmed_bytes, total_bytes):
"""Updates session histograms with the latest statistics.
This method calculates and updates the histograms for SNR, bytes per
minute (BPM), bits per second (BPS), and time, using the provided
confirmed and total bytes. It limits the histogram size to the last
20 entries.
Args:
confirmed_bytes: The number of confirmed bytes transmitted.
total_bytes: The total number of bytes transmitted.
Returns:
dict: A dictionary containing the calculated session statistics.
"""
stats = self.calculate_session_statistics(confirmed_bytes, total_bytes)
self.snr_histogram.append(self.snr)
self.bpm_histogram.append(stats['bytes_per_minute'])
self.bps_histogram.append(stats['bits_per_second'])
self.time_histogram.append(datetime.datetime.now().isoformat())
# Limit the size of each histogram to the last 20 entries
self.snr_histogram = self.snr_histogram[-20:]
self.bpm_histogram = self.bpm_histogram[-20:]
self.bps_histogram = self.bps_histogram[-20:]
self.time_histogram = self.time_histogram[-20:]
return stats
def check_channel_busy(self, channel_busy_slot, mode_slot):
"""Checks if the channel is busy based on channel busy slots and mode slots.
This method iterates through the channel_busy_slot and mode_slot lists.
It returns False if a channel is both busy and the mode is active,
indicating the channel is available. Otherwise, it returns True.
Args:
channel_busy_slot: A list of booleans representing channel busy status.
mode_slot: A list of booleans representing mode activity status.
Returns:
bool: True if the channel is available, False otherwise.
"""
for busy, mode in zip(channel_busy_slot, mode_slot):
if busy and mode:
return False
return True
def get_appropriate_speed_level(self, snr, maximum_bandwidth=None):
"""
Determines the appropriate speed level based on the SNR, channel busy slot, and maximum bandwidth.
Parameters:
- snr (float): The signal-to-noise ratio.
- channel_busy_slot (list of bool): The busy condition of the channels.
- maximum_bandwidth (float, optional): The maximum bandwidth. If None, uses the default from the configuration.
Returns:
- int: The appropriate speed level.
"""
# Use default maximum bandwidth from configuration if not provided
if maximum_bandwidth is None:
maximum_bandwidth = self.ctx.config_manager.config['MODEM']['maximum_bandwidth']
# Adjust maximum_bandwidth if set to 0 (use maximum available bandwidth from speed levels)
if maximum_bandwidth == 0:
maximum_bandwidth = max(details['bandwidth'] for details in self.SPEED_LEVEL_DICT.values())
# Iterate through speed levels in reverse order to find the highest appropriate one
for level in sorted(self.SPEED_LEVEL_DICT.keys(), reverse=True):
details = self.SPEED_LEVEL_DICT[level]
mode_slots = details['slots'].value
if (snr >= details['min_snr'] and
details['bandwidth'] <= maximum_bandwidth and
self.check_channel_busy(self.ctx.state_manager.channel_busy_slot, mode_slots)):
return level
# Return the lowest level if no higher level is found
return min(self.SPEED_LEVEL_DICT.keys())
def reset_session(self):
"""Resets the ARQ session to its initial state.
This method clears all session-related data, including received bytes,
histograms, type byte, total length, CRC values, received data,
maximum bandwidth, and the abort flag.
"""
self.received_bytes = 0
self.snr_histogram = []
self.bpm_histogram = []
self.bps_histogram = []
self.time_histogram = []
self.type_byte = None
self.total_length = 0
self.total_crc = ''
self.received_data = None
self.received_bytes = 0
self.received_crc = None
self.maximum_bandwidth = 0
self.abort = False