Merge branch 'develop' into dev-interface-tests

This commit is contained in:
DJ2LS 2024-03-25 19:04:37 +01:00
commit e24a64ba25
15 changed files with 96 additions and 88 deletions

View file

@ -2,7 +2,7 @@
"name": "FreeDATA", "name": "FreeDATA",
"description": "FreeDATA Client application for connecting to FreeDATA server", "description": "FreeDATA Client application for connecting to FreeDATA server",
"private": true, "private": true,
"version": "0.14.4-alpha", "version": "0.14.5-alpha",
"main": "dist-electron/main/index.js", "main": "dist-electron/main/index.js",
"scripts": { "scripts": {
"start": "vite", "start": "vite",
@ -77,7 +77,7 @@
"vite": "5.1.3", "vite": "5.1.3",
"vite-plugin-electron": "0.28.2", "vite-plugin-electron": "0.28.2",
"vite-plugin-electron-renderer": "0.14.5", "vite-plugin-electron-renderer": "0.14.5",
"vitest": "1.2.2", "vitest": "1.3.1",
"vue": "3.4.21", "vue": "3.4.21",
"vue-tsc": "1.8.27" "vue-tsc": "1.8.27"
} }

View file

@ -124,27 +124,6 @@ import { loadAllData } from "../js/eventHandler";
<!-------------------------------- MAIN AREA ----------------> <!-------------------------------- MAIN AREA ---------------->
<!------------------------------------------------------------------------------------------> <!------------------------------------------------------------------------------------------>
<div class="container">
<div class="row">
<div class="col-5">
<main_active_rig_control />
</div>
<div class="col-4">
<main_active_broadcasts />
</div>
<div class="col-3">
<main_active_audio_level />
</div>
</div>
<div class="row">
<div class="col-7">
<main_active_heard_stations />
</div>
<div class="col-5">
<main_active_stats />
</div>
</div>
</div>
</div> </div>
</div> </div>

View file

@ -165,20 +165,20 @@ const audioStore = useAudioStore();
</div> </div>
<div class="input-group input-group-sm mb-1"> <div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Enable 250Hz bandwidth mode</label> <label class="input-group-text w-50">Maximum used bandwidth</label>
<label class="input-group-text w-50"> <select
<div class="form-check form-switch form-check-inline"> class="form-select form-select-sm"
<input id="maximum_bandwidth"
class="form-check-input" @change="onChange"
type="checkbox" v-model.number="settings.remote.MODEM.maximum_bandwidth"
id="250HzModeSwitch" >
v-model="settings.remote.MODEM.enable_low_bandwidth_mode" <option value="250">250 Hz</option>
@change="onChange" <option value="563">563 Hz</option>
/> <option value="1700">1700 Hz</option>
<label class="form-check-label" for="250HzModeSwitch">250Hz</label> </select>
</div>
</label>
</div> </div>
<div class="input-group input-group-sm mb-1"> <div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Respond to CQ</label> <label class="input-group-text w-50">Respond to CQ</label>
<label class="input-group-text w-50"> <label class="input-group-text w-50">

View file

@ -54,11 +54,11 @@ const defaultConfig = {
enable_protocol: false, enable_protocol: false,
}, },
MODEM: { MODEM: {
enable_low_bandwidth_mode: false,
respond_to_cq: false, respond_to_cq: false,
tx_delay: 0, tx_delay: 0,
enable_hamc: false, enable_hamc: false,
enable_morse_identifier: false, enable_morse_identifier: false,
maximum_bandwidth: 3000,
}, },
RADIO: { RADIO: {
control: "disabled", control: "disabled",

View file

@ -1,5 +1,5 @@
import datetime import datetime
import queue, threading import threading
import codec2 import codec2
import data_frame_factory import data_frame_factory
import structlog import structlog
@ -9,23 +9,26 @@ import time
from arq_data_type_handler import ARQDataTypeHandler from arq_data_type_handler import ARQDataTypeHandler
class ARQSession(): class ARQSession:
SPEED_LEVEL_DICT = { SPEED_LEVEL_DICT = {
0: { 0: {
'mode': codec2.FREEDV_MODE.datac4, 'mode': codec2.FREEDV_MODE.datac4,
'min_snr': -10, 'min_snr': -10,
'duration_per_frame': 5.17, 'duration_per_frame': 5.17,
'bandwidth': 250,
}, },
1: { 1: {
'mode': codec2.FREEDV_MODE.datac3, 'mode': codec2.FREEDV_MODE.datac3,
'min_snr': 0, 'min_snr': 0,
'duration_per_frame': 3.19, 'duration_per_frame': 3.19,
'bandwidth': 563,
}, },
2: { 2: {
'mode': codec2.FREEDV_MODE.datac1, 'mode': codec2.FREEDV_MODE.datac1,
'min_snr': 3, 'min_snr': 3,
'duration_per_frame': 4.18, 'duration_per_frame': 4.18,
'bandwidth': 1700,
}, },
} }
@ -63,7 +66,7 @@ class ARQSession():
self.bpm_histogram = [] self.bpm_histogram = []
self.time_histogram = [] self.time_histogram = []
def log(self, message, isWarning = False): def log(self, message, isWarning=False):
msg = f"[{type(self).__name__}][id={self.id}][state={self.state}]: {message}" msg = f"[{type(self).__name__}][id={self.id}][state={self.state}]: {message}"
logger = self.logger.warn if isWarning else self.logger.info logger = self.logger.warn if isWarning else self.logger.info
logger(msg) logger(msg)
@ -99,21 +102,22 @@ class ARQSession():
self.event_frame_received.set() self.event_frame_received.set()
self.log(f"Received {frame['frame_type']}") self.log(f"Received {frame['frame_type']}")
frame_type = frame['frame_type_int'] frame_type = frame['frame_type_int']
if self.state in self.STATE_TRANSITION: if self.state in self.STATE_TRANSITION and frame_type in self.STATE_TRANSITION[self.state]:
if frame_type in self.STATE_TRANSITION[self.state]: action_name = self.STATE_TRANSITION[self.state][frame_type]
action_name = self.STATE_TRANSITION[self.state][frame_type] received_data, type_byte = getattr(self, action_name)(frame)
received_data, type_byte = getattr(self, action_name)(frame) if isinstance(received_data, bytearray) and isinstance(type_byte, int):
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)))
self.arq_data_type_handler.dispatch(type_byte, received_data, self.update_histograms(len(received_data), len(received_data))) return
return
self.log(f"Ignoring unknown transition from state {self.state.name} with frame {frame['frame_type']}") self.log(f"Ignoring unknown transition from state {self.state.name} with frame {frame['frame_type']}")
def is_session_outdated(self): def is_session_outdated(self):
session_alivetime = time.time() - self.session_max_age session_alivetime = time.time() - self.session_max_age
if self.session_ended < session_alivetime and self.state.name in ['FAILED', 'ENDED', 'ABORTED']: return self.session_ended < session_alivetime and self.state.name in [
return True 'FAILED',
return False 'ENDED',
'ABORTED',
]
def calculate_session_duration(self): def calculate_session_duration(self):
if self.session_ended == 0: if self.session_ended == 0:
@ -123,7 +127,7 @@ class ARQSession():
def calculate_session_statistics(self, confirmed_bytes, total_bytes): def calculate_session_statistics(self, confirmed_bytes, total_bytes):
duration = self.calculate_session_duration() duration = self.calculate_session_duration()
#total_bytes = self.total_length # total_bytes = self.total_length
# self.total_length # self.total_length
duration_in_minutes = duration / 60 # Convert duration from seconds to minutes duration_in_minutes = duration / 60 # Convert duration from seconds to minutes
@ -134,9 +138,9 @@ class ARQSession():
bytes_per_minute = 0 bytes_per_minute = 0
# Convert histograms lists to dictionaries # Convert histograms lists to dictionaries
time_histogram_dict = {i: timestamp for i, timestamp in enumerate(self.time_histogram)} time_histogram_dict = dict(enumerate(self.time_histogram))
snr_histogram_dict = {i: snr for i, snr in enumerate(self.snr_histogram)} snr_histogram_dict = dict(enumerate(self.snr_histogram))
bpm_histogram_dict = {i: bpm for i, bpm in enumerate(self.bpm_histogram)} bpm_histogram_dict = dict(enumerate(self.bpm_histogram))
return { return {
'total_bytes': total_bytes, 'total_bytes': total_bytes,
@ -160,11 +164,20 @@ class ARQSession():
return stats return stats
def get_appropriate_speed_level(self, snr): def get_appropriate_speed_level(self, snr, maximum_bandwidth=None):
# Start with the lowest speed level as default if maximum_bandwidth is None:
# In case of a not fitting SNR, we return the lowest speed level maximum_bandwidth = self.config['MODEM']['maximum_bandwidth']
# Adjust maximum_bandwidth based on special conditions or invalid configurations
if maximum_bandwidth == 0:
# Use the maximum available bandwidth from the speed level dictionary
maximum_bandwidth = max(details['bandwidth'] for details in self.SPEED_LEVEL_DICT.values())
# Initialize appropriate_speed_level to the lowest level that meets the minimum criteria
appropriate_speed_level = min(self.SPEED_LEVEL_DICT.keys()) appropriate_speed_level = min(self.SPEED_LEVEL_DICT.keys())
for level, details in self.SPEED_LEVEL_DICT.items(): for level, details in self.SPEED_LEVEL_DICT.items():
if snr >= details['min_snr'] and level > appropriate_speed_level: if snr >= details['min_snr'] and details['bandwidth'] <= maximum_bandwidth and level > appropriate_speed_level:
appropriate_speed_level = level appropriate_speed_level = level
return appropriate_speed_level
return appropriate_speed_level

View file

@ -18,7 +18,7 @@ class IRS_State(Enum):
class ARQSessionIRS(arq_session.ARQSession): class ARQSessionIRS(arq_session.ARQSession):
TIMEOUT_CONNECT = 55 #14.2 TIMEOUT_CONNECT = 55 #14.2
TIMEOUT_DATA = 60 TIMEOUT_DATA = 120
STATE_TRANSITION = { STATE_TRANSITION = {
IRS_State.NEW: { IRS_State.NEW: {
@ -76,14 +76,15 @@ class ARQSessionIRS(arq_session.ARQSession):
self.received_bytes = 0 self.received_bytes = 0
self.received_crc = None self.received_crc = None
self.maximum_bandwidth = 0
self.abort = False self.abort = False
def all_data_received(self): def all_data_received(self):
return self.total_length == self.received_bytes return self.total_length == self.received_bytes
def final_crc_matches(self) -> bool: def final_crc_matches(self) -> bool:
match = self.total_crc == helpers.get_crc_32(bytes(self.received_data)).hex() return self.total_crc == helpers.get_crc_32(bytes(self.received_data)).hex()
return match
def transmit_and_wait(self, frame, timeout, mode): def transmit_and_wait(self, frame, timeout, mode):
self.event_frame_received.clear() self.event_frame_received.clear()
@ -99,6 +100,12 @@ class ARQSessionIRS(arq_session.ARQSession):
thread_wait.start() thread_wait.start()
def send_open_ack(self, open_frame): def send_open_ack(self, open_frame):
self.maximum_bandwidth = open_frame['maximum_bandwidth']
# check for maximum bandwidth. If ISS bandwidth is higher than own, then use own
if open_frame['maximum_bandwidth'] > self.config['MODEM']['maximum_bandwidth']:
self.maximum_bandwidth = self.config['MODEM']['maximum_bandwidth']
self.event_manager.send_arq_session_new( self.event_manager.send_arq_session_new(
False, self.id, self.dxcall, 0, self.state.name) False, self.id, self.dxcall, 0, self.state.name)
ack_frame = self.frame_factory.build_arq_session_open_ack( ack_frame = self.frame_factory.build_arq_session_open_ack(
@ -212,7 +219,7 @@ class ARQSessionIRS(arq_session.ARQSession):
received_speed_level = 0 received_speed_level = 0
latest_snr = self.snr if self.snr else -10 latest_snr = self.snr if self.snr else -10
appropriate_speed_level = self.get_appropriate_speed_level(latest_snr) appropriate_speed_level = self.get_appropriate_speed_level(latest_snr, self.maximum_bandwidth)
modes_to_decode = {} modes_to_decode = {}
# Log the latest SNR, current, appropriate speed levels, and the previous speed level # Log the latest SNR, current, appropriate speed levels, and the previous speed level
@ -247,7 +254,7 @@ class ARQSessionIRS(arq_session.ARQSession):
return self.speed_level return self.speed_level
def abort_transmission(self): def abort_transmission(self):
self.log(f"Aborting transmission... setting abort flag") self.log("Aborting transmission... setting abort flag")
self.abort = True self.abort = True
def send_stop_ack(self, stop_frame): def send_stop_ack(self, stop_frame):
@ -263,7 +270,7 @@ class ARQSessionIRS(arq_session.ARQSession):
# final function for failed transmissions # final function for failed transmissions
self.session_ended = time.time() self.session_ended = time.time()
self.set_state(IRS_State.FAILED) self.set_state(IRS_State.FAILED)
self.log(f"Transmission failed!") self.log("Transmission failed!")
self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,False, self.state.name, statistics=self.calculate_session_statistics(self.received_bytes, self.total_length)) self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,False, self.state.name, statistics=self.calculate_session_statistics(self.received_bytes, self.total_length))
self.states.setARQ(False) self.states.setARQ(False)
return None, None return None, None

View file

@ -1,6 +1,5 @@
import threading import threading
import data_frame_factory import data_frame_factory
import queue
import random import random
from codec2 import FREEDV_MODE from codec2 import FREEDV_MODE
from modem_frametypes import FRAME_TYPE from modem_frametypes import FRAME_TYPE
@ -105,9 +104,10 @@ class ARQSessionISS(arq_session.ARQSession):
twr.start() twr.start()
def start(self): def start(self):
maximum_bandwidth = self.config['MODEM']['maximum_bandwidth']
self.event_manager.send_arq_session_new( self.event_manager.send_arq_session_new(
True, self.id, self.dxcall, self.total_length, self.state.name) 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) session_open_frame = self.frame_factory.build_arq_session_open(self.dxcall, self.id, maximum_bandwidth)
self.launch_twr(session_open_frame, self.TIMEOUT_CONNECT_ACK, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling) self.launch_twr(session_open_frame, self.TIMEOUT_CONNECT_ACK, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)
self.set_state(ISS_State.OPEN_SENT) self.set_state(ISS_State.OPEN_SENT)
@ -174,7 +174,7 @@ class ARQSessionISS(arq_session.ARQSession):
payload_size = self.get_data_payload_size() payload_size = self.get_data_payload_size()
burst = [] burst = []
for f in range(0, self.frames_per_burst): for _ in range(0, self.frames_per_burst):
offset = self.confirmed_bytes offset = self.confirmed_bytes
payload = self.data[offset : offset + payload_size] payload = self.data[offset : offset + payload_size]
data_frame = self.frame_factory.build_arq_burst_frame( data_frame = self.frame_factory.build_arq_burst_frame(
@ -204,7 +204,7 @@ class ARQSessionISS(arq_session.ARQSession):
# final function for failed transmissions # final function for failed transmissions
self.session_ended = time.time() self.session_ended = time.time()
self.set_state(ISS_State.FAILED) self.set_state(ISS_State.FAILED)
self.log(f"Transmission failed!") self.log("Transmission failed!")
self.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.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.states.setARQ(False) self.states.setARQ(False)
@ -213,7 +213,7 @@ class ARQSessionISS(arq_session.ARQSession):
def abort_transmission(self, irs_frame=None): def abort_transmission(self, irs_frame=None):
# function for starting the abort sequence # function for starting the abort sequence
self.log(f"aborting transmission...") self.log("aborting transmission...")
self.set_state(ISS_State.ABORTING) self.set_state(ISS_State.ABORTING)
self.event_manager.send_arq_session_finished( self.event_manager.send_arq_session_finished(

View file

@ -45,10 +45,10 @@ enable_protocol = False
[MODEM] [MODEM]
enable_hmac = False enable_hmac = False
enable_low_bandwidth_mode = False
enable_morse_identifier = False enable_morse_identifier = False
respond_to_cq = True respond_to_cq = True
tx_delay = 200 tx_delay = 50
maximum_bandwidth = 1700
[SOCKET_INTERFACE] [SOCKET_INTERFACE]
enable = False enable = False

View file

@ -57,7 +57,7 @@ class CONFIG:
'MODEM': { 'MODEM': {
'enable_hmac': bool, 'enable_hmac': bool,
'enable_morse_identifier': bool, 'enable_morse_identifier': bool,
'enable_low_bandwidth_mode': bool, 'maximum_bandwidth': int,
'respond_to_cq': bool, 'respond_to_cq': bool,
'tx_delay': int 'tx_delay': int
}, },

View file

@ -100,6 +100,7 @@ class DataFrameFactory:
"destination_crc": 3, "destination_crc": 3,
"origin": 6, "origin": 6,
"session_id": 1, "session_id": 1,
"maximum_bandwidth": 2,
} }
self.template_list[FR_TYPE.ARQ_SESSION_OPEN_ACK.value] = { self.template_list[FR_TYPE.ARQ_SESSION_OPEN_ACK.value] = {
@ -278,7 +279,7 @@ class DataFrameFactory:
elif key in ["session_id", "speed_level", elif key in ["session_id", "speed_level",
"frames_per_burst", "version", "frames_per_burst", "version",
"offset", "total_length", "state", "type"]: "offset", "total_length", "state", "type", "maximum_bandwidth"]:
extracted_data[key] = int.from_bytes(data, 'big') extracted_data[key] = int.from_bytes(data, 'big')
elif key in ["snr"]: elif key in ["snr"]:
@ -387,11 +388,12 @@ class DataFrameFactory:
test_frame[:1] = bytes([FR_TYPE.TEST_FRAME.value]) test_frame[:1] = bytes([FR_TYPE.TEST_FRAME.value])
return test_frame return test_frame
def build_arq_session_open(self, destination, session_id): def build_arq_session_open(self, destination, session_id, maximum_bandwidth):
payload = { payload = {
"destination_crc": helpers.get_crc_24(destination), "destination_crc": helpers.get_crc_24(destination),
"origin": helpers.callsign_to_bytes(self.myfullcall), "origin": helpers.callsign_to_bytes(self.myfullcall),
"session_id": session_id.to_bytes(1, 'big'), "session_id": session_id.to_bytes(1, 'big'),
"maximum_bandwidth": maximum_bandwidth.to_bytes(2, 'big'),
} }
return self.construct(FR_TYPE.ARQ_SESSION_OPEN, payload) return self.construct(FR_TYPE.ARQ_SESSION_OPEN, payload)

View file

@ -33,7 +33,7 @@ class explorer():
callsign = str(self.config['STATION']['mycall']) + "-" + str(self.config["STATION"]['myssid']) callsign = str(self.config['STATION']['mycall']) + "-" + str(self.config["STATION"]['myssid'])
gridsquare = str(self.config['STATION']['mygrid']) gridsquare = str(self.config['STATION']['mygrid'])
version = str(self.modem_version) version = str(self.modem_version)
bandwidth = str(self.config['MODEM']['enable_low_bandwidth_mode']) bandwidth = str(self.config['MODEM']['maximum_bandwidth'])
beacon = str(self.states.is_beacon_running) beacon = str(self.states.is_beacon_running)
strength = str(self.states.s_meter_strength) strength = str(self.states.s_meter_strength)

View file

@ -5,11 +5,22 @@ import frame_handler
from message_system_db_messages import DatabaseManagerMessages from message_system_db_messages import DatabaseManagerMessages
class CQFrameHandler(frame_handler_ping.PingFrameHandler): class CQFrameHandler(frame_handler.FrameHandler):
def should_respond(self): #def should_respond(self):
self.logger.debug(f"Respond to CQ: {self.config['MODEM']['respond_to_cq']}") # self.logger.debug(f"Respond to CQ: {self.config['MODEM']['respond_to_cq']}")
return self.config['MODEM']['respond_to_cq'] # return bool(self.config['MODEM']['respond_to_cq'] and not self.states.getARQ())
def follow_protocol(self):
if self.states.getARQ():
return
self.logger.debug(
f"[Modem] Responding to request from [{self.details['frame']['origin']}]",
snr=self.details['snr'],
)
self.send_ack()
def send_ack(self): def send_ack(self):
factory = data_frame_factory.DataFrameFactory(self.config) factory = data_frame_factory.DataFrameFactory(self.config)

View file

@ -15,15 +15,11 @@ class PingFrameHandler(frame_handler.FrameHandler):
# ft = self.details['frame']['frame_type'] # ft = self.details['frame']['frame_type']
# self.logger.info(f"[Modem] {ft} received but not for us.") # self.logger.info(f"[Modem] {ft} received but not for us.")
# return valid # return valid
#def should_respond(self):
# return self.is_frame_for_me()
def follow_protocol(self): def follow_protocol(self):
if not bool(self.is_frame_for_me() and not self.states.getARQ()):
if not self.should_respond():
return return
self.logger.debug( self.logger.debug(
f"[Modem] Responding to request from [{self.details['frame']['origin']}]", f"[Modem] Responding to request from [{self.details['frame']['origin']}]",
snr=self.details['snr'], snr=self.details['snr'],

View file

@ -33,7 +33,7 @@ from schedule_manager import ScheduleManager
app = Flask(__name__) app = Flask(__name__)
CORS(app, resources={r"/*": {"origins": "*"}}) CORS(app, resources={r"/*": {"origins": "*"}})
sock = Sock(app) sock = Sock(app)
MODEM_VERSION = "0.14.4-alpha" MODEM_VERSION = "0.14.5-alpha"
# set config file to use # set config file to use
def set_config(): def set_config():

View file

@ -32,7 +32,7 @@ class TestDataFrameFactory(unittest.TestCase):
def testARQConnect(self): def testARQConnect(self):
dxcall = "DJ2LS-4" dxcall = "DJ2LS-4"
session_id = 123 session_id = 123
frame = self.factory.build_arq_session_open(dxcall, session_id) frame = self.factory.build_arq_session_open(dxcall, session_id, 1700)
frame_data = self.factory.deconstruct(frame) frame_data = self.factory.deconstruct(frame)
self.assertEqual(frame_data['origin'], self.factory.myfullcall) self.assertEqual(frame_data['origin'], self.factory.myfullcall)