FreeDATA/freedata_server/arq_session_iss.py

489 lines
22 KiB
Python

import threading
import data_frame_factory
import random
from codec2 import FREEDV_MODE
from modem_frametypes import FRAME_TYPE
import arq_session
import helpers
from enum import Enum
import time
import stats
class ISS_State(Enum):
"""Enumeration representing the states of an ISS (Information Sending Station) ARQ session.
This enumeration defines the various states that an ARQ session on the
Information Sending Station (ISS) side can transition through during
the data transfer process.
Attributes:
NEW: Initial state of a new session.
OPEN_SENT: State after sending an ARQ session open request.
INFO_SENT: State after sending the session information frame.
BURST_SENT: State after sending a burst data frame.
ENDED: State after successfully transmitting all data and receiving confirmation.
FAILED: State after a session failure, such as a timeout or abort.
ABORTING: State while running the abort sequence and waiting for a stop acknowledgement.
ABORTED: State after receiving a stop acknowledgement, indicating session termination.
"""
NEW = 0
OPEN_SENT = 1
INFO_SENT = 2
BURST_SENT = 3
ENDED = 4
FAILED = 5
ABORTING = 6 # state while running abort sequence and waiting for stop ack
ABORTED = 7 # stop ack received
class ARQSessionISS(arq_session.ARQSession):
"""Manages an ARQ session on the Information Sending Station (ISS) side.
This class extends the base ARQSession and handles the transmission of data
using the ARQ protocol. It manages session state, retries, timeouts, and
data transfer.
"""
RETRIES_CONNECT = 5
RETRIES_INFO = 10
RETRIES_DATA = 25
RETRIES_STOP = 5
# DJ2LS: 3 seconds seems to be too small for radios with a too slow PTT toggle time
# DJ2LS: 3.5 seconds is working well WITHOUT a channel busy detection delay
TIMEOUT_CHANNEL_BUSY = 0
TIMEOUT_CONNECT_ACK = 4.5 + TIMEOUT_CHANNEL_BUSY
TIMEOUT_TRANSFER = 3.5 + TIMEOUT_CHANNEL_BUSY
TIMEOUT_STOP_ACK = 4.5 + TIMEOUT_CHANNEL_BUSY
STATE_TRANSITION = {
ISS_State.OPEN_SENT: {
FRAME_TYPE.ARQ_SESSION_OPEN_ACK.value: 'send_info',
},
ISS_State.INFO_SENT: {
FRAME_TYPE.ARQ_SESSION_OPEN_ACK.value: 'send_info',
FRAME_TYPE.ARQ_SESSION_INFO_ACK.value: 'send_data',
},
ISS_State.BURST_SENT: {
FRAME_TYPE.ARQ_SESSION_INFO_ACK.value: 'send_data',
FRAME_TYPE.ARQ_BURST_ACK.value: 'send_data',
},
ISS_State.FAILED:{
FRAME_TYPE.ARQ_STOP_ACK.value: 'transmission_aborted'
},
ISS_State.ABORTING: {
FRAME_TYPE.ARQ_STOP_ACK.value: 'transmission_aborted',
},
ISS_State.ABORTED: {
FRAME_TYPE.ARQ_STOP_ACK.value: 'transmission_aborted',
}
}
def __init__(self, ctx, dxcall: str, data: bytearray, type_byte: bytes):
"""Initializes a new ARQ session on the Information Sending Station (ISS) side.
This method sets up the ARQ session for the ISS, initializing session
parameters, data, CRC, state, and frame factory. It also enables the
decoder for signalling ACK bursts.
Args:
config (dict): The configuration dictionary.
modem: The modem object.
dxcall (str): The DX call sign.
state_manager: The state manager object.
data (bytearray): The data to be transmitted.
type_byte (bytes): The type byte of the data.
"""
super().__init__(ctx, dxcall)
self.ctx = ctx
self.dxcall = dxcall
self.data = data
self.total_length = len(data)
self.data_crc = helpers.get_crc_32(self.data)
self.type_byte = type_byte
self.confirmed_bytes = 0
self.expected_byte_offset = 0
# instance of p2p connection
self.running_p2p_connection = None
self.state = ISS_State.NEW
self.state_enum = ISS_State # needed for access State enum from outside
self.id = self.generate_id()
self.is_IRS = False
# enable decoder for signalling ACK bursts
self.ctx.rf_modem.demodulator.set_decode_mode(modes_to_decode=None, is_arq_irs=False)
self.frame_factory = data_frame_factory.DataFrameFactory(self.ctx)
def generate_id(self):
"""Generates a unique session ID.
This method attempts to generate a unique 8-bit session ID. It first
checks for existing sessions with matching CRC to allow resuming
interrupted transmissions. If no match is found, it generates a new
ID based on the data CRC and ensures it's not already in use.
Returns:
int: The generated session ID (1-255), or False if all IDs are exhausted.
"""
# Iterate through existing sessions to find a matching CRC
for session_id, session_data in self.ctx.state_manager.arq_iss_sessions.items():
if session_data.data_crc == self.data_crc and session_data.state in [ISS_State.FAILED, ISS_State.ABORTED]:
# If a matching CRC is found, use this session ID
self.log(f"Matching CRC found, deleting existing session and resuming transmission", isWarning=True)
self.ctx.state_manager.remove_arq_iss_session(session_id)
return session_id
self.log(f"No matching CRC found, creating new session id", isWarning=False)
# Compute 8-bit integer from the 32-bit CRC
# Convert the byte sequence to a 32-bit integer (little-endian)
checksum_int = int.from_bytes(self.data_crc, byteorder='little')
random_int = checksum_int % 256
# Check if the generated 8-bit integer can be used
if random_int not in self.ctx.state_manager.arq_iss_sessions:
return random_int
# If the generated ID is already used, generate a new random ID
while True:
random_int = random.randint(1, 255)
if random_int not in self.ctx.state_manager.arq_iss_sessions:
return random_int
if len(self.ctx.state_manager.arq_iss_sessions) >= 255:
# Return False if all possible session IDs are exhausted
return False
def transmit_wait_and_retry(self, frame_or_burst, timeout, retries, mode, isARQBurst=False):
"""Transmits a frame or burst, waits for a response, and retries if necessary.
This method transmits the given frame or burst of frames, waits for a
response event, and retries the transmission if a timeout occurs.
It handles retries up to the specified limit and implements a fallback
mechanism for ARQ bursts by switching to a lower speed level if
necessary.
Args:
frame_or_burst: The frame or list of frames to be transmitted.
timeout (float): The timeout period in seconds.
retries (int): The maximum number of retries.
mode: The FreeDV mode to use for transmission.
isARQBurst (bool, optional): True if transmitting an ARQ burst, False otherwise. Defaults to False.
"""
while retries > 0 and self.state not in [ISS_State.ABORTED, ISS_State.ABORTING]:
self.event_frame_received = threading.Event()
if isinstance(frame_or_burst, list): burst = frame_or_burst
else: burst = [frame_or_burst]
for f in burst:
self.transmit_frame(f, mode)
self.event_frame_received.clear()
self.log(f"Waiting {timeout} seconds...")
if self.event_frame_received.wait(timeout):
return
self.log("Timeout!")
retries = retries - 1
# TODO TEMPORARY TEST FOR SENDING IN LOWER SPEED LEVEL IF WE HAVE TWO FAILED TRANSMISSIONS!!!
if retries == self.RETRIES_DATA - 2 and isARQBurst and self.speed_level > 0 and self.state not in [ISS_State.ABORTED, ISS_State.ABORTING]:
self.log("SENDING IN FALLBACK SPEED LEVEL", isWarning=True)
self.speed_level = 0
print(f" CONFIRMED BYTES: {self.confirmed_bytes}")
self.send_data({'flag':{'ABORT': False, 'FINAL': False}, 'speed_level': self.speed_level}, fallback=True)
return
self.set_state(ISS_State.FAILED)
self.transmission_failed()
def launch_twr(self, frame_or_burst, timeout, retries, mode, isARQBurst=False):
"""Launches the transmit_wait_and_retry method in a separate thread.
Creates and starts a daemon thread to execute the transmit_wait_and_retry
method. This allows the transmission and retry process to occur in the
background without blocking the main thread.
Args:
frame_or_burst: The frame or burst of frames to transmit.
timeout (float): The timeout for each transmission attempt.
retries (int): The number of transmission retries to attempt.
mode: The FreeDV mode to use for transmission.
isARQBurst (bool, optional): True if the transmission is an ARQ burst, False otherwise. Defaults to False.
"""
twr = threading.Thread(target = self.transmit_wait_and_retry, args=[frame_or_burst, timeout, retries, mode, isARQBurst], daemon=True)
twr.start()
def start(self):
"""Starts the ARQ session.
This method initiates the ARQ session by sending a session open frame
to the IRS and setting the session state to OPEN_SENT. It also sends
an ARQ session new event.
"""
maximum_bandwidth = self.ctx.config_manager.config['MODEM']['maximum_bandwidth']
print(maximum_bandwidth)
self.ctx.event_manager.send_arq_session_new(
True, self.id, self.dxcall, self.total_length, self.state.name)
session_open_frame = self.frame_factory.build_arq_session_open(self.dxcall, self.id, maximum_bandwidth, self.protocol_version)
self.launch_twr(session_open_frame, self.TIMEOUT_CONNECT_ACK, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)
self.set_state(ISS_State.OPEN_SENT)
def update_speed_level(self, frame):
"""Updates the transmission speed level based on the received frame.
This method extracts the speed level from the received frame and updates
the session's speed level accordingly. It logs the speed level change
and handles cases where the received speed level is outside the
allowable range.
Args:
frame: The received frame containing the new speed level.
"""
self.log("---------------------------------------------------------", isWarning=True)
# Log the received frame for debugging
self.log(f"Received frame: {frame}", isWarning=True)
# Extract the speed_level directly from the frame
if 'speed_level' in frame:
new_speed_level = frame['speed_level']
# Ensure the new speed level is within the allowable range
if 0 <= new_speed_level < len(self.SPEED_LEVEL_DICT):
# Log the speed level change if it's different from the current speed level
if new_speed_level != self.speed_level:
self.log(f"Changing speed level from {self.speed_level} to {new_speed_level}", isWarning=True)
self.speed_level = new_speed_level # Update the current speed level
else:
self.log("Received speed level is the same as the current speed level.", isWarning=True)
else:
self.log(f"Received speed level {new_speed_level} is out of allowable range.", isWarning=True)
else:
self.log("No speed level specified in the received frame.", isWarning=True)
def send_info(self, irs_frame):
"""Sends the session information frame to the IRS.
This method builds and sends the ARQ_SESSION_INFO frame containing
details about the data to be transmitted, such as total length, CRC,
SNR, and data type. It also handles transmission retries and aborts
based on the received IRS frame.
Args:
irs_frame: The received frame from the IRS.
Returns:
Tuple[None, None]: Returns None for both data and type_byte as this method doesn't handle data.
"""
# check if we received an abort flag
if irs_frame["flag"]["ABORT"]:
return self.transmission_aborted(irs_frame=irs_frame)
info_frame = self.frame_factory.build_arq_session_info(self.id, self.total_length,
self.data_crc,
self.snr, self.type_byte)
self.launch_twr(info_frame, self.TIMEOUT_CONNECT_ACK, self.RETRIES_INFO, mode=FREEDV_MODE.signalling)
self.set_state(ISS_State.INFO_SENT)
return None, None
def send_data(self, irs_frame, fallback=None):
"""Sends data bursts to the IRS.
This method handles sending data bursts to the IRS, managing speed
level adjustments, acknowledgements, and session progress updates.
It also handles transmission aborts and session completion or failure.
Args:
irs_frame: The received frame from the IRS.
fallback (bool, optional): Indicates if this is a fallback transmission attempt at a lower speed level. Defaults to None.
Returns:
Tuple[None, None]: Returns None for both data and type_byte as this method doesn't handle data directly.
"""
if 'offset' in irs_frame:
self.log(f"received data offset: {irs_frame['offset']}", isWarning=True)
self.expected_byte_offset = irs_frame['offset']
# interrupt transmission when aborting
if self.state in [ISS_State.ABORTED, ISS_State.ABORTING]:
#self.event_frame_received.set()
#self.send_stop()
return
# update statistics
self.update_histograms(self.confirmed_bytes, self.total_length)
self.update_speed_level(irs_frame)
# update p2p connection timeout
if self.running_p2p_connection:
self.running_p2p_connection.last_data_timestamp = time.time()
if self.expected_byte_offset > self.total_length:
self.confirmed_bytes = self.total_length
elif not fallback:
self.confirmed_bytes = self.expected_byte_offset
self.log(f"IRS confirmed {self.confirmed_bytes}/{self.total_length} bytes")
self.ctx.event_manager.send_arq_session_progress(True, self.id, self.dxcall, self.confirmed_bytes, self.total_length, self.state.name, self.speed_level, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
# check if we received an abort flag
if irs_frame["flag"]["ABORT"]:
self.transmission_aborted(irs_frame=irs_frame)
return None, None
if irs_frame["flag"]["FINAL"]:
if self.confirmed_bytes == self.total_length and irs_frame["flag"]["CHECKSUM"]:
self.transmission_ended(irs_frame)
else:
self.transmission_failed()
return None, None
payload_size = self.get_data_payload_size()
burst = []
for _ in range(0, self.frames_per_burst):
offset = self.confirmed_bytes
#self.expected_byte_offset = offset
payload = self.data[offset : offset + payload_size]
#self.expected_byte_offset = offset + payload_size
self.expected_byte_offset = offset + len(payload)
#print(f"EXPECTED----------------------{self.expected_byte_offset}")
data_frame = self.frame_factory.build_arq_burst_frame(
self.SPEED_LEVEL_DICT[self.speed_level]["mode"],
self.id, offset, payload, self.speed_level)
burst.append(data_frame)
self.launch_twr(burst, self.TIMEOUT_TRANSFER, self.RETRIES_DATA, mode='auto', isARQBurst=True)
self.set_state(ISS_State.BURST_SENT)
return None, None
def transmission_ended(self, irs_frame):
"""Handles the successful completion of the transmission.
This method is called when the transmission ends successfully. It sets
the session state to ENDED, logs the completion, sends session finished
events, transmits session statistics, and cleans up the session.
Args:
irs_frame: The received IRS frame.
Returns:
Tuple[None, None]: Returns None for both data and type_byte.
"""
# final function for sucessfully ended transmissions
self.session_ended = time.time()
self.set_state(ISS_State.ENDED)
self.log(f"All data transfered! flag_final={irs_frame['flag']['FINAL']}, flag_checksum={irs_frame['flag']['CHECKSUM']}")
self.ctx.event_manager.send_arq_session_finished(True, self.id, self.dxcall,True, self.state.name, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
#print(self.ctx.state_manager.p2p_connection_sessions)
#print(self.arq_data_type_handler.state_manager.p2p_connection_sessions)
session_stats = self.calculate_session_statistics(self.confirmed_bytes, self.total_length)
self.arq_data_type_handler.transmitted(self.type_byte, self.data, session_stats)
self.ctx.state_manager.remove_arq_iss_session(self.id)
self.ctx.state_manager.setARQ(False)
return None, None
def transmission_failed(self, irs_frame=None):
"""Handles transmission failures.
This method is called when a transmission fails. It sets the session
state to FAILED, logs the failure, sends session finished events,
and disables ARQ. It also notifies the ARQ data type handler about
the failure.
Args:
irs_frame (optional): The received IRS frame, if any. Defaults to None.
Returns:
Tuple[None, None]: Returns None for both data and type_byte.
"""
# final function for failed transmissions
self.session_ended = time.time()
self.set_state(ISS_State.FAILED)
self.log("Transmission failed!")
session_stats=self.calculate_session_statistics(self.confirmed_bytes, self.total_length)
self.ctx.event_manager.send_arq_session_finished(True, self.id, self.dxcall,False, self.state.name, session_stats)
self.ctx.state_manager.setARQ(False)
self.arq_data_type_handler.failed(self.type_byte, self.data, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
return None, None
def abort_transmission(self, send_stop=False, irs_frame=None):
"""Starts the ARQ transmission abort sequence.
This method initiates the abort sequence, sets the session state to
ABORTING, sends session finished events, clears the audio output queue,
and optionally sends a stop frame after a delay.
Args:
send_stop (bool, optional): Whether to send an ARQ_STOP frame. Defaults to False.
irs_frame (optional): The received IRS frame, if any. Defaults to None.
"""
self.log("aborting transmission...")
self.set_state(ISS_State.ABORTING)
self.ctx.event_manager.send_arq_session_finished(
True, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
# clear audio out queue
self.ctx.rf_modem.audio_out_queue.queue.clear()
# break actual retries
self.event_frame_received.set()
# wait for transmit function to be ready before setting event
while self.ctx.state_manager.isTransmitting():
threading.Event().wait(0.100)
# break actual retries
self.event_frame_received.set()
if send_stop:
# sleep some time for avoiding packet collission
threading.Event().wait(self.TIMEOUT_STOP_ACK)
self.send_stop()
self.ctx.state_manager.setARQ(False)
def send_stop(self):
"""Sends an ARQ stop frame.
This method builds and sends an ARQ_STOP frame to the IRS, initiating
the termination of the ARQ session. It uses the launch_twr method
for transmission and retries.
"""
stop_frame = self.frame_factory.build_arq_stop(self.id)
self.launch_twr(stop_frame, self.TIMEOUT_STOP_ACK, self.RETRIES_STOP, mode=FREEDV_MODE.signalling)
def transmission_aborted(self, irs_frame=None):
"""Handles the abortion of the transmission.
This method is called when the transmission is aborted. It sets the
session state to ABORTED, logs the abortion, sends session finished
events, and disables ARQ.
Args:
irs_frame (optional): The received IRS frame, if any. Defaults to None.
Returns:
Tuple[None, None]: Returns None for both data and type_byte.
"""
# Only run this part, if we are not already aborted or ended the session.
if self.state not in [ISS_State.ABORTED, ISS_State.ENDED]:
self.log("session aborted")
self.session_ended = time.time()
self.set_state(ISS_State.ABORTED)
# break actual retries
self.event_frame_received.set()
self.ctx.event_manager.send_arq_session_finished(
True, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
#self.ctx.state_manager.remove_arq_iss_session(self.id)
self.ctx.state_manager.setARQ(False)
return None, None