mirror of
https://github.com/DJ2LS/FreeDATA
synced 2024-05-14 08:04:33 +00:00
WIP ARQ
This commit is contained in:
parent
ad11332d16
commit
000702740f
9 changed files with 360 additions and 42 deletions
92
modem/arq_session_irs.py
Normal file
92
modem/arq_session_irs.py
Normal 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
135
modem/arq_session_iss.py
Normal 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()
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = {
|
||||
|
|
17
modem/frame_handler_arq.py
Normal file
17
modem/frame_handler_arq.py
Normal 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()
|
|
@ -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
71
tests/test_arq_session.py
Normal 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()
|
Loading…
Reference in a new issue