This commit is contained in:
Pedro 2023-12-05 15:40:04 +01:00
parent ad11332d16
commit 000702740f
9 changed files with 360 additions and 42 deletions

92
modem/arq_session_irs.py Normal file
View file

@ -0,0 +1,92 @@
import threading
import data_frame_factory
import queue
class ARQSessionIRS():
STATE_CONN_REQ_RECEIVED = 0
STATE_WAITING_DATA = 1
STATE_FAILED = 2
STATE_ENDED = 10
RETRIES_CONNECT = 3
RETRIES_TRANSFER = 3
TIMEOUT_DATA = 2
def __init__(self, config: dict, tx_frame_queue: queue.Queue, dxcall: str, session_id: int):
self.config = config
self.tx_frame_queue = tx_frame_queue
self.dxcall = dxcall
self.session_id = session_id
self.received_data = b''
self.state = self.STATE_CONN_REQ_RECEIVED
self.event_data_received = threading.Event()
self.frame_factory = data_frame_factory.DataFrameFactory(self.config)
def generate_id(self):
pass
def log(self, message):
pass
def set_state(self, state):
self.log(f"ARQ Session {self.id} state {self.state}")
self.state = state
def transmit_frame(self, frame: bytearray):
self.tx_frame_queue.put(frame)
def set_modem_decode_modes(self, modes):
pass
def runner(self, request):
isWideband = True
speed = 1
version = 1
ack_frame = self.frame_factory.build_arq_connect_ack(isWideband, self.session_id, speed, version)
self.transmit_frame(ack_frame)
self.set_modem_decode_modes(None)
self.state = self.STATE_WAITING_DATA
while self.state == self.STATE_WAITING_DATA:
if not self.event_data_received.wait(self.TIMEOUT_DATA):
self.log("Timeout waiting for data")
self.state = self.STATE_FAILED
return
self.log("Finished ARQ IRS session")
def run(self):
self.thread = threading.Thread(target=self.runner, name=f"ARQ IRS Session {self.id}", daemon=True)
self.thread.run()
def on_data_received(self, data_frame):
if self.state != self.STATE_WAITING_DATA:
raise RuntimeError(f"ARQ Session: Received data while in state {self.state}")
self.event_data_received.set()
def on_transfer_ack_received(self, ack):
self.event_transfer_ack_received.set()
self.speed_level = ack['speed_level']
def on_transfer_nack_received(self, nack):
self.speed_level = nack['speed_level']
def on_disconnect_received(self):
self.abort()
def abort(self):
self.state = self.STATE_DISCONNECTED
self.event_connection_ack_received.set()
self.event_connection_ack_received.clear()
self.event_transfer_feedback.set()
self.event_transfer_feedback.clear()

135
modem/arq_session_iss.py Normal file
View file

@ -0,0 +1,135 @@
import threading
import data_frame_factory
import queue
import random
class ARQSessionISS():
STATE_DISCONNECTED = 0
STATE_CONNECTING = 1
STATE_CONNECTED = 2
STATE_SENDING = 3
STATE_ENDED = 10
RETRIES_CONNECT = 3
RETRIES_TRANSFER = 3
TIMEOUT_CONNECT_ACK = 5
TIMEOUT_TRANSFER = 2
def __init__(self, config: dict, tx_frame_queue: queue.Queue, dxcall: str, data: bytearray):
self.config = config
self.tx_frame_queue = tx_frame_queue
self.dxcall = dxcall
self.data = data
self.state = self.STATE_DISCONNECTED
self.id = self.generate_id()
self.event_connection_ack_received = threading.Event()
self.event_transfer_ack_received = threading.Event()
self.frame_factory = data_frame_factory.DataFrameFactory(self.config)
def generate_id(self):
return random.randint(1,255)
def log(self, message):
pass
def set_state(self, state):
self.log(f"ARQ Session {self.id} state {self.state}")
self.state = state
def transmit_frame(self, frame: bytearray):
modem_queue_item = {
'mode': self.mode,
'repeat': 1,
'repeat_delay': 1,
'frame': frame,
}
self.tx_frame_queue.put(modem_queue_item)
def runner(self):
if not self.connect():
return False
return self.send_data()
def run(self):
self.thread = threading.Thread(target=self.runner, name=f"ARQ ISS Session {self.id}", daemon=True)
self.thread.run()
def connect(self):
self.set_state(self.STATE_CONNECTING)
connect_frame = self.frame_factory.build_arq_session_connect(True, self.dxcall, self.id)
retries = self.RETRIES_CONNECT
while retries > 0:
self.transmit_frame(connect_frame)
if self.event_connection_ack_received.wait(self.TIMEOUT_CONNECT_ACK):
self.setState(self.STATE_CONNECTED)
return True
retries = retries - 1
self.setState(self.STATE_DISCONNECTED)
return False
def on_connection_ack_received(self, ack):
if self.state != self.STATE_CONNECTING:
raise RuntimeError(f"ARQ Session: Received connection ACK while in state {self.state}")
self.speed_level = ack['speed_level']
self.event_connection_ack_received.set()
# Sends the full payload in multiple frames
def send_data(self):
offset = 0
while offset < len(self.payload):
max_size = self.get_max_size_for_speed_level(self.speed_level)
end_offset = min(len(self.payload), max_size)
frame_payload = self.payload[offset:end_offset]
data_frame = self.frame_factory.build_arq_session_send(self.speed_level,
self.dxcall,
frame_payload)
self.set_state(self.STATE_SENDING)
if not self.send_arq(data_frame):
return False
offset = end_offset + 1
# Send part of the payload using ARQ
def send_arq(self, frame):
retries = self.RETRIES_TRANSFER
while retries > 0:
# to know later if it has changed
speed_level = self.speed_level
self.transmit_frame(frame)
# wait for ack
if self.event_transfer_ack_received.wait(self.TIMEOUT_TRANSFER):
speed_level = self.speed_level
return True
# don't decrement retries if speed level is changing
if self.speed_level == speed_level:
retries = retries - 1
self.set_state(self.STATE_DISCONNECTED)
return False
def on_transfer_ack_received(self, ack):
self.event_transfer_ack_received.set()
self.speed_level = ack['speed_level']
def on_transfer_nack_received(self, nack):
self.speed_level = nack['speed_level']
def on_disconnect_received(self):
self.abort()
def abort(self):
self.state = self.STATE_DISCONNECTED
self.event_connection_ack_received.set()
self.event_connection_ack_received.clear()
self.event_transfer_feedback.set()
self.event_transfer_feedback.clear()

View file

@ -2,10 +2,11 @@ from data_frame_factory import DataFrameFactory
import queue
from codec2 import FREEDV_MODE
import structlog
from state_manager import StateManager
class TxCommand():
def __init__(self, config, state_manager, modem_events, apiParams = {}):
def __init__(self, config: dict, state_manager: StateManager, modem_events: queue.Queue, apiParams:dict = {}):
self.config = config
self.logger = structlog.get_logger("Command")
self.state_manager = state_manager

View file

@ -1,36 +1,23 @@
import queue
from command import TxCommand
import api_validations
from protocol_arq_iss import ISS
from protocol_arq import ARQ
import base64
from queue import Queue
from arq_session_iss import ARQSessionISS
class ARQRawCommand(TxCommand):
def __int__(self, state_manager):
# open a new arq instance here
self.initialize_arq_instance()
def set_params_from_api(self, apiParams):
self.dxcall = apiParams['dxcall']
if not api_validations.validate_freedata_callsign(self.dxcall):
self.dxcall = f"{self.dxcall}-0"
return super().set_params_from_api(apiParams)
def initialize_arq_transmission_iss(self, data):
if id := self.get_id_from_frame(data):
instance = self.initialize_arq_instance()
self.states.register_arq_instance_by_id(id, instance)
instance['arq_irs'].arq_received_data_channel_opener()
self.data = base64.b64decode(apiParams['data'])
def run(self, event_queue: Queue, tx_frame_queue: Queue):
self.emit_event(event_queue)
self.logger.info(self.log_message())
def initialize_arq_instance(self):
self.arq = ARQ(self.config, self.event_queue, self.state_manager)
self.arq_iss = ISS(self.config, self.event_queue, self.state_manager)
return {
'arq': self.arq,
'arq_irs': self.arq_irs,
'arq_iss': self.arq_iss,
'arq_session': self.arq_session
}
def build_frame(self):
return self.frame_factory.build_arq_connect(destination=self.dxcall, session_id=b'', isWideband=True)
iss = ARQSessionISS(self.config, tx_frame_queue, self.dxcall, self.data)
self.state_manager.register_arq_iss_session(iss)
iss.run()
return iss

View file

@ -329,7 +329,8 @@ class DataFrameFactory:
"origin": helpers.callsign_to_bytes(self.myfullcall),
"session_id": session_id.to_bytes(1, 'big'),
}
return self.construct(FR_TYPE.ARQ_SESSION_OPEN, payload)
channel_type = FR_TYPE.ARQ_DC_OPEN_W if isWideband else FR_TYPE.ARQ_DC_OPEN_N
return self.construct(channel_type, payload)
def build_arq_burst_ack(self, session_id: bytes, snr: int, speed_level: int, len_arq_rx_frame_buffer: int):
# ack_frame = bytearray(self.length_sig1_frame)

View file

@ -9,15 +9,13 @@ from codec2 import FREEDV_MODE
class FrameHandler():
def __init__(self, name: str, config, states: StateManager, event_manager: EventManager,
tx_frame_queue: Queue,
arq_sessions: list) -> None:
tx_frame_queue: Queue) -> None:
self.name = name
self.config = config
self.states = states
self.event_manager = event_manager
self.tx_frame_queue = tx_frame_queue
self.arq_sessions = arq_sessions
self.logger = structlog.get_logger("Frame Handler")
self.details = {

View file

@ -0,0 +1,17 @@
from queue import Queue
import frame_handler
from modem.event_manager import EventManager
from modem.state_manager import StateManager
from modem_frametypes import FRAME_TYPE as FR
from arq_session_irs import ARQSessionIRS
class ARQFrameHandler(frame_handler.FrameHandler):
def follow_protocol(self):
frame = self.details['frame']
# ARQ session open received
if frame.frame_type in [FR.ARQ_DC_OPEN_N.value, FR.ARQ_DC_OPEN_W]:
session = ARQSessionIRS(self.config, self.tx_frame_queue, frame.origin, frame.session_id)
self.states.register_arq_irs_session(session)
session.run()

View file

@ -33,7 +33,9 @@ class StateManager:
self.heard_stations = [] # TODO remove it... heard stations list == deprecated
self.activities_list = {}
self.arq_instance_table = {}
self.arq_iss_sessions = {}
self.arq_irs_sessions = {}
self.arq_session_state = 'disconnected'
self.arq_speed_level = 0
self.arq_total_bytes = 0
@ -112,19 +114,33 @@ class StateManager:
def waitForTransmission(self):
self.transmitting_event.wait()
def register_arq_instance_by_id(self, id, instance):
self.arq_instance_table[id] = instance
def register_arq_iss_session(self, session):
if session.id in self.arq_iss_sessions:
raise RuntimeError(f"ARQ ISS Session '{session.id}' already exists!")
self.arq_iss_sessions[session.id] = session
def get_arq_instance_by_id(self, id):
return self.arq_instance_table.get(id)
def register_arq_irs_session(self, session):
if session.id in self.arq_irs_sessions:
raise RuntimeError(f"ARQ IRS Session '{session.id}' already exists!")
self.arq_irs_sessions[session.id] = session
def delete_arq_instance_by_id(self, id):
instances = self.arq_instance_table.pop(id, None)
if None not in instances:
for key in instances:
del instances[key]
return True
return False
def get_arq_iss_session(self, id):
if id not in self.arq_iss_sessions:
raise RuntimeError(f"ARQ ISS Session '{id}' not found!")
def get_arq_irs_session(self, id):
if id not in self.arq_irs_sessions:
raise RuntimeError(f"ARQ IRS Session '{id}' not found!")
def remove_arq_iss_session(self, id):
if id not in self.arq_iss_sessions:
raise RuntimeError(f"ARQ ISS Session '{id}' not found!")
del self.arq_iss_sessions[id]
def remove_arq_irs_session(self, id):
if id not in self.arq_irs_sessions:
raise RuntimeError(f"ARQ ISS Session '{id}' not found!")
del self.arq_irs_sessions[id]
def add_activity(self, activity_data):
# Generate a random 8-byte string as hex

71
tests/test_arq_session.py Normal file
View file

@ -0,0 +1,71 @@
import sys
sys.path.append('modem')
import unittest
from config import CONFIG
import helpers
import queue
import threading
import base64
from command_arq_raw import ARQRawCommand
from state_manager import StateManager
from frame_dispatcher import DISPATCHER
class TestARQSession(unittest.TestCase):
@classmethod
def setUpClass(cls):
config_manager = CONFIG('modem/config.ini.example')
cls.config = config_manager.read()
# ISS
cls.iss_modem_transmit_queue = queue.Queue()
cls.iss_state_manager = StateManager(queue.Queue())
cls.iss_event_queue = queue.Queue()
cls.iss_frame_dispatcher = DISPATCHER(cls.config,
cls.iss_event_queue,
cls.iss_state_manager,
queue.Queue(),
cls.iss_modem_transmit_queue)
# IRS
cls.irs_modem_transmit_queue = queue.Queue()
cls.irs_state_manager = StateManager(queue.Queue())
cls.irs_event_queue = queue.Queue()
cls.irs_frame_dispatcher = DISPATCHER(cls.config,
cls.irs_event_queue,
cls.irs_state_manager,
queue.Queue(),
cls.irs_modem_transmit_queue)
def channelWorker(self, modem_transmit_queue: queue, frame_dispatcher: DISPATCHER):
while True:
transmission_item = modem_transmit_queue.get()
frame_bytes = bytes(transmission_item['frame'])
frame_dispatcher.new_process_data(frame_bytes, None, len(frame_bytes), 0, 0)
def establishChannels(self):
self.iss_to_irs_channel = threading.Thread(target=self.channelWorker,
args=[self.iss_modem_transmit_queue,
self.irs_frame_dispatcher],
name = "IRS to ISS channel")
self.iss_to_irs_channel.start()
self.irs_to_iss_channel = threading.Thread(target=self.channelWorker,
args=[self.irs_modem_transmit_queue,
self.iss_frame_dispatcher],
name = "IRS to ISS channel")
self.irs_to_iss_channel.start()
def testARQSession(self):
self.establishChannels()
params = {
'dxcall': "DJ2LS-3",
'data': base64.b64encode(bytes("Hello world!", encoding="utf-8")),
}
cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params)
cmd.run(self.iss_event_queue, self.iss_modem_transmit_queue)
if __name__ == '__main__':
unittest.main()