mirror of https://github.com/DJ2LS/FreeDATA.git
Merge branch 'develop' into dev-connected-mode
commit
ae364ac57d
|
@ -158,9 +158,25 @@ export default {
|
|||
},
|
||||
|
||||
parsedMessageBody() {
|
||||
// Use marked to parse markdown and DOMPurify to sanitize
|
||||
return DOMPurify.sanitize(marked.parse(this.message.body));
|
||||
},
|
||||
// Parse markdown to HTML
|
||||
let parsedHTML = marked.parse(this.message.body);
|
||||
|
||||
// Sanitize the HTML
|
||||
let sanitizedHTML = DOMPurify.sanitize(parsedHTML);
|
||||
|
||||
// Create a temporary DOM element to manipulate the sanitized output
|
||||
let tempDiv = document.createElement("div");
|
||||
tempDiv.innerHTML = sanitizedHTML;
|
||||
|
||||
// Modify all links to open in a new tab
|
||||
tempDiv.querySelectorAll("a").forEach(link => {
|
||||
link.setAttribute("target", "_blank");
|
||||
link.setAttribute("rel", "noopener noreferrer"); // Security best practice
|
||||
});
|
||||
|
||||
// Return the updated HTML
|
||||
return tempDiv.innerHTML;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -202,8 +202,26 @@ export default {
|
|||
},
|
||||
|
||||
parsedMessageBody() {
|
||||
// Use marked to parse markdown and DOMPurify to sanitize
|
||||
return DOMPurify.sanitize(marked.parse(this.message.body));
|
||||
// Parse markdown to HTML
|
||||
let parsedHTML = marked.parse(this.message.body);
|
||||
|
||||
// Sanitize the HTML
|
||||
let sanitizedHTML = DOMPurify.sanitize(parsedHTML);
|
||||
|
||||
// Create a temporary DOM element to manipulate the sanitized output
|
||||
let tempDiv = document.createElement("div");
|
||||
tempDiv.innerHTML = sanitizedHTML;
|
||||
|
||||
// Modify all links to open in a new tab
|
||||
tempDiv.querySelectorAll("a").forEach(link => {
|
||||
link.setAttribute("target", "_blank");
|
||||
link.setAttribute("rel", "noopener noreferrer"); // Security best practice
|
||||
});
|
||||
|
||||
// Return the updated HTML
|
||||
return tempDiv.innerHTML;
|
||||
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -74,6 +74,11 @@ function handleFiles(files) {
|
|||
((file.size - compressedFile.size) / file.size * 100).toFixed(2) + '%'
|
||||
);
|
||||
|
||||
// Check if compression made the file larger
|
||||
if (compressedFile.size >= file.size) {
|
||||
console.warn("Compressed file is larger than original. Using original file instead.");
|
||||
compressedFile = file; // Use original file
|
||||
}
|
||||
// toast notification
|
||||
let message = `
|
||||
<div>
|
||||
|
|
|
@ -52,6 +52,33 @@ export default {
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Enable ADIF export -->
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<label class="input-group-text w-50 text-wrap">
|
||||
Enable ADIF
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link p-0 ms-2"
|
||||
data-bs-toggle="tooltip"
|
||||
title="Enable ADIF via UDP"
|
||||
>
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</button>
|
||||
</label>
|
||||
<label class="input-group-text w-50">
|
||||
<div class="form-check form-switch form-check-inline ms-2">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="enableADIFSwitch"
|
||||
@change="onChange"
|
||||
v-model="settings.remote.QSO_LOGGING.enable_adif_udp"
|
||||
/>
|
||||
<label class="form-check-label" for="enableADIFSwitch">Enable</label>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- ADIF Log Host -->
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<label class="input-group-text w-50 text-wrap">
|
||||
|
@ -71,7 +98,7 @@ export default {
|
|||
placeholder="Enter ADIF server host"
|
||||
id="adifLogHost"
|
||||
@change="onChange"
|
||||
v-model="settings.remote.MESSAGES.adif_log_host"
|
||||
v-model="settings.remote.QSO_LOGGING.adif_udp_host"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -96,7 +123,78 @@ export default {
|
|||
max="65534"
|
||||
min="1025"
|
||||
@change="onChange"
|
||||
v-model.number="settings.remote.MESSAGES.adif_log_port"
|
||||
v-model.number="settings.remote.QSO_LOGGING.adif_udp_port"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Enable Wavelog API -->
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<label class="input-group-text w-50 text-wrap">
|
||||
Enable Wavelog API
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link p-0 ms-2"
|
||||
data-bs-toggle="tooltip"
|
||||
title="Enable Wavelog API"
|
||||
>
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</button>
|
||||
</label>
|
||||
<label class="input-group-text w-50">
|
||||
<div class="form-check form-switch form-check-inline ms-2">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="enableWavelogSwitch"
|
||||
@change="onChange"
|
||||
v-model="settings.remote.QSO_LOGGING.enable_adif_wavelog"
|
||||
/>
|
||||
<label class="form-check-label" for="enableWavelogSwitch">Enable</label>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Wavelog Host -->
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<label class="input-group-text w-50 text-wrap">
|
||||
Wavelog API Host
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link p-0 ms-2"
|
||||
data-bs-toggle="tooltip"
|
||||
title="Wavelog API server host, e.g., 127.0.0.1"
|
||||
>
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</button>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Enter wavelog server host"
|
||||
id="wavelogHost"
|
||||
@change="onChange"
|
||||
v-model="settings.remote.QSO_LOGGING.adif_wavelog_host"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<label class="input-group-text w-50 text-wrap">
|
||||
Wavelog API key
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link p-0 ms-2"
|
||||
data-bs-toggle="tooltip"
|
||||
title="Wavelog API key"
|
||||
>
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</button>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Enter Wavelog api key"
|
||||
id="wavelogApiKey"
|
||||
@change="onChange"
|
||||
v-model="settings.remote.QSO_LOGGING.adif_wavelog_api_key"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import socket
|
||||
|
||||
import re
|
||||
import structlog
|
||||
|
||||
|
||||
def send_adif_qso_data(config, adif_data):
|
||||
|
@ -11,16 +12,25 @@ def send_adif_qso_data(config, adif_data):
|
|||
server_port (int): Port of the server.
|
||||
adif_data (str): ADIF-formatted QSO data.
|
||||
"""
|
||||
adif_log_host = config['MESSAGES'].get('adif_log_host', '127.0.0.1')
|
||||
adif_log_port = int(config['MESSAGES'].get('adif_log_port', '2237'))
|
||||
|
||||
log = structlog.get_logger()
|
||||
|
||||
# If False then exit the function
|
||||
adif = config['QSO_LOGGING'].get('enable_adif_udp', 'False')
|
||||
|
||||
if not adif:
|
||||
return # exit as we don't want to log ADIF UDP
|
||||
|
||||
adif_log_host = config['QSO_LOGGING'].get('adif_udp_host', '127.0.0.1')
|
||||
adif_log_port = int(config['QSO_LOGGING'].get('adif_udp_port', '2237'))
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
|
||||
# Send the ADIF data to the server
|
||||
sock.sendto(adif_data.encode('utf-8'), (adif_log_host, adif_log_port))
|
||||
print(f"ADIF QSO data sent to {adif_log_host}:{adif_log_port}", adif_data.encode('utf-8'))
|
||||
log.info(f"[CHAT] ADIF QSO data sent to: {adif_log_host}:{adif_log_port} {adif_data.encode('utf-8')}")
|
||||
except Exception as e:
|
||||
print(f"Error sending ADIF data: {e}")
|
||||
log.info(f"[CHAT] Error sending ADIF data: {e}")
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
|
|
@ -34,7 +34,10 @@ class FREEDV_MODE(Enum):
|
|||
datac4 = 18
|
||||
datac13 = 19
|
||||
datac14 = 20
|
||||
data_ofdm_200 = 21200
|
||||
data_ofdm_250 = 21250
|
||||
data_ofdm_500 = 21500
|
||||
data_ofdm_1700 = 211700
|
||||
data_ofdm_2438 = 2124381
|
||||
#data_qam_2438 = 2124382
|
||||
#qam16c2 = 22
|
||||
|
@ -51,7 +54,10 @@ class FREEDV_MODE_USED_SLOTS(Enum):
|
|||
datac4 = [False, False, True, False, False]
|
||||
datac13 = [False, False, True, False, False]
|
||||
datac14 = [False, False, True, False, False]
|
||||
data_ofdm_200 = [False, False, True, False, False]
|
||||
data_ofdm_250 = [False, False, True, False, False]
|
||||
data_ofdm_500 = [False, False, True, False, False]
|
||||
data_ofdm_1700 = [False, True, True, True, False]
|
||||
data_ofdm_2438 = [True, True, True, True, True]
|
||||
data_qam_2438 = [True, True, True, True, True]
|
||||
qam16c2 = [True, True, True, True, True]
|
||||
|
@ -375,10 +381,9 @@ class resampler:
|
|||
|
||||
return out48
|
||||
|
||||
|
||||
def open_instance(mode: int) -> ctypes.c_void_p:
|
||||
data_custom = 21
|
||||
if mode in [FREEDV_MODE.data_ofdm_500.value, FREEDV_MODE.data_ofdm_2438.value]:
|
||||
if mode in [FREEDV_MODE.data_ofdm_200.value, FREEDV_MODE.data_ofdm_250.value, FREEDV_MODE.data_ofdm_500.value, FREEDV_MODE.data_ofdm_1700.value, FREEDV_MODE.data_ofdm_2438.value]:
|
||||
#if mode in [FREEDV_MODE.data_ofdm_500.value, FREEDV_MODE.data_ofdm_2438.value, FREEDV_MODE.data_qam_2438]:
|
||||
custom_params = ofdm_configurations[mode]
|
||||
return ctypes.cast(
|
||||
|
@ -535,6 +540,49 @@ def create_tx_uw(nuwbits, uw_sequence):
|
|||
|
||||
# ---------------- OFDM 500 Hz Bandwidth ---------------#
|
||||
|
||||
# DATAC13 # OFDM 200
|
||||
data_ofdm_200_config = create_default_ofdm_config()
|
||||
data_ofdm_200_config.config.contents.ns = 5
|
||||
data_ofdm_200_config.config.contents.np = 18
|
||||
data_ofdm_200_config.config.contents.tcp = 0.006
|
||||
data_ofdm_200_config.config.contents.ts = 0.016
|
||||
data_ofdm_200_config.config.contents.rs = 1.0 / data_ofdm_200_config.config.contents.ts
|
||||
data_ofdm_200_config.config.contents.nc = 3
|
||||
data_ofdm_200_config.config.contents.timing_mx_thresh = 0.45
|
||||
data_ofdm_200_config.config.contents.bad_uw_errors = 18
|
||||
data_ofdm_200_config.config.contents.codename = "H_256_512_4".encode('utf-8')
|
||||
data_ofdm_200_config.config.contents.amp_scale = 2.5*300E3
|
||||
data_ofdm_200_config.config.contents.nuwbits = 48
|
||||
data_ofdm_200_config.config.contents.tx_uw = create_tx_uw(data_ofdm_200_config.config.contents.nuwbits, [1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0])
|
||||
data_ofdm_200_config.config.contents.clip_gain1 = 1.2
|
||||
data_ofdm_200_config.config.contents.clip_gain2 = 1.0
|
||||
data_ofdm_200_config.config.contents.tx_bpf_en = False
|
||||
data_ofdm_200_config.config.contents.tx_bpf_proto = codec2_filter_coeff.generate_filter_coefficients(8000, 400, 101)
|
||||
data_ofdm_200_config.config.contents.tx_bpf_proto_n = 101 # TODO sizeof(filtP200S400) / sizeof(float);
|
||||
|
||||
|
||||
# DATAC4 # OFDM 250
|
||||
data_ofdm_250_config = create_default_ofdm_config()
|
||||
data_ofdm_250_config.config.contents.ns = 5
|
||||
data_ofdm_250_config.config.contents.np = 47
|
||||
data_ofdm_250_config.config.contents.tcp = 0.006
|
||||
data_ofdm_250_config.config.contents.ts = 0.016
|
||||
data_ofdm_250_config.config.contents.rs = 1.0 / data_ofdm_250_config.config.contents.ts
|
||||
data_ofdm_250_config.config.contents.nc = 4
|
||||
data_ofdm_250_config.config.contents.timing_mx_thresh = 0.5
|
||||
data_ofdm_250_config.config.contents.bad_uw_errors = 12
|
||||
data_ofdm_250_config.config.contents.codename = "H_1024_2048_4f".encode('utf-8')
|
||||
data_ofdm_250_config.config.contents.amp_scale = 2*300E3
|
||||
data_ofdm_250_config.config.contents.nuwbits = 32
|
||||
data_ofdm_250_config.config.contents.tx_uw = create_tx_uw(data_ofdm_250_config.config.contents.nuwbits, [1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0])
|
||||
data_ofdm_250_config.config.contents.clip_gain1 = 1.2
|
||||
data_ofdm_250_config.config.contents.clip_gain2 = 1.0
|
||||
data_ofdm_250_config.config.contents.tx_bpf_en = True
|
||||
data_ofdm_250_config.config.contents.tx_bpf_proto = codec2_filter_coeff.generate_filter_coefficients(8000, 400, 101)
|
||||
data_ofdm_250_config.config.contents.tx_bpf_proto_n = 101 # TODO sizeof(filtP200S400) / sizeof(float);
|
||||
|
||||
|
||||
# OFDM 500
|
||||
data_ofdm_500_config = create_default_ofdm_config()
|
||||
data_ofdm_500_config.config.contents.ns = 5
|
||||
data_ofdm_500_config.config.contents.np = 32
|
||||
|
@ -545,36 +593,35 @@ data_ofdm_500_config.config.contents.nc = 8
|
|||
data_ofdm_500_config.config.contents.timing_mx_thresh = 0.1
|
||||
data_ofdm_500_config.config.contents.bad_uw_errors = 18
|
||||
data_ofdm_500_config.config.contents.codename = "H_1024_2048_4f".encode('utf-8')
|
||||
data_ofdm_500_config.config.contents.amp_scale = 290E3
|
||||
data_ofdm_500_config.config.contents.amp_scale = 300E3 # 290E3
|
||||
data_ofdm_500_config.config.contents.nuwbits = 56
|
||||
data_ofdm_500_config.config.contents.tx_uw = create_tx_uw(data_ofdm_500_config.config.contents.nuwbits, [0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1])
|
||||
data_ofdm_500_config.config.contents.clip_gain1 = 2.8
|
||||
data_ofdm_500_config.config.contents.clip_gain2 = 0.9
|
||||
data_ofdm_500_config.config.contents.tx_bpf_en = False
|
||||
data_ofdm_500_config.config.contents.clip_gain1 = 2.5 # 2.8
|
||||
data_ofdm_500_config.config.contents.clip_gain2 = 1.0 #0.9
|
||||
data_ofdm_500_config.config.contents.tx_bpf_en = True
|
||||
data_ofdm_500_config.config.contents.tx_bpf_proto = codec2_filter_coeff.generate_filter_coefficients(8000, 600, 100)
|
||||
data_ofdm_500_config.config.contents.tx_bpf_proto_n = 100
|
||||
|
||||
|
||||
"""
|
||||
# DATAC1
|
||||
data_ofdm_500_config = create_default_ofdm_config()
|
||||
data_ofdm_500_config.config.contents.ns = 5
|
||||
data_ofdm_500_config.config.contents.np = 38
|
||||
data_ofdm_500_config.config.contents.tcp = 0.006
|
||||
data_ofdm_500_config.config.contents.ts = 0.016
|
||||
data_ofdm_500_config.config.contents.nc = 27
|
||||
data_ofdm_500_config.config.contents.nuwbits = 16
|
||||
data_ofdm_500_config.config.contents.timing_mx_thresh = 0.10
|
||||
data_ofdm_500_config.config.contents.bad_uw_errors = 6
|
||||
data_ofdm_500_config.config.contents.codename = b"H_4096_8192_3d"
|
||||
data_ofdm_500_config.config.contents.clip_gain1 = 2.7
|
||||
data_ofdm_500_config.config.contents.clip_gain2 = 0.8
|
||||
data_ofdm_500_config.config.contents.amp_scale = 145E3
|
||||
data_ofdm_500_config.config.contents.tx_bpf_en = False
|
||||
#data_ofdm_500_config.config.contents.tx_bpf_proto = codec2_filter_coeff.generate_filter_coefficients(8000, 2000, 101)
|
||||
data_ofdm_500_config.config.contents.tx_bpf_proto_n = 101
|
||||
data_ofdm_500_config.config.contents.tx_uw = create_tx_uw(data_ofdm_500_config.config.contents.nuwbits, [1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0])
|
||||
"""
|
||||
# DATAC1 # OFDM1700
|
||||
data_ofdm_1700_config = create_default_ofdm_config()
|
||||
data_ofdm_1700_config.config.contents.ns = 5
|
||||
data_ofdm_1700_config.config.contents.np = 38
|
||||
data_ofdm_1700_config.config.contents.tcp = 0.006
|
||||
data_ofdm_1700_config.config.contents.ts = 0.016
|
||||
data_ofdm_1700_config.config.contents.nc = 27
|
||||
data_ofdm_1700_config.config.contents.nuwbits = 16
|
||||
data_ofdm_1700_config.config.contents.timing_mx_thresh = 0.10
|
||||
data_ofdm_1700_config.config.contents.bad_uw_errors = 6
|
||||
data_ofdm_1700_config.config.contents.codename = b"H_4096_8192_3d"
|
||||
data_ofdm_1700_config.config.contents.clip_gain1 = 2.7
|
||||
data_ofdm_1700_config.config.contents.clip_gain2 = 0.8
|
||||
data_ofdm_1700_config.config.contents.amp_scale = 145E3
|
||||
data_ofdm_1700_config.config.contents.tx_bpf_en = False
|
||||
data_ofdm_1700_config.config.contents.tx_bpf_proto = codec2_filter_coeff.generate_filter_coefficients(8000, 2000, 100)
|
||||
data_ofdm_1700_config.config.contents.tx_bpf_proto_n = 100
|
||||
data_ofdm_1700_config.config.contents.tx_uw = create_tx_uw(data_ofdm_1700_config.config.contents.nuwbits, [1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0])
|
||||
|
||||
|
||||
"""
|
||||
# DATAC3
|
||||
|
@ -639,7 +686,10 @@ data_qam_2438_config.config.contents.tx_uw = create_tx_uw(162, [1, 1, 0, 0, 1, 0
|
|||
"""
|
||||
|
||||
ofdm_configurations = {
|
||||
FREEDV_MODE.data_ofdm_200.value: data_ofdm_200_config,
|
||||
FREEDV_MODE.data_ofdm_250.value: data_ofdm_250_config,
|
||||
FREEDV_MODE.data_ofdm_500.value: data_ofdm_500_config,
|
||||
FREEDV_MODE.data_ofdm_1700.value: data_ofdm_1700_config,
|
||||
FREEDV_MODE.data_ofdm_2438.value: data_ofdm_2438_config,
|
||||
#FREEDV_MODE.data_qam_2438.value: data_qam_2438_config
|
||||
|
||||
|
|
|
@ -63,8 +63,14 @@ data_port = 8001
|
|||
|
||||
[MESSAGES]
|
||||
enable_auto_repeat = False
|
||||
adif_log_host = 127.0.0.1
|
||||
adif_log_port = 2237
|
||||
|
||||
[QSO_LOGGING]
|
||||
enable_adif_udp = False
|
||||
adif_udp_host = 127.0.0.1
|
||||
adif_udp_port = 2237
|
||||
enable_adif_wavelog = False
|
||||
adif_wavelog_host = http://raspberrypi
|
||||
adif_wavelog_api_key = API-KEY
|
||||
|
||||
[GUI]
|
||||
auto_run_browser = True
|
||||
|
|
|
@ -2,6 +2,7 @@ import configparser
|
|||
import structlog
|
||||
import json
|
||||
|
||||
|
||||
class CONFIG:
|
||||
"""
|
||||
CONFIG class for handling with config files
|
||||
|
@ -68,18 +69,24 @@ class CONFIG:
|
|||
'enable_socket_interface': bool,
|
||||
},
|
||||
'SOCKET_INTERFACE': {
|
||||
'enable' : bool,
|
||||
'host' : str,
|
||||
'cmd_port' : int,
|
||||
'data_port' : int,
|
||||
|
||||
'enable': bool,
|
||||
'host': str,
|
||||
'cmd_port': int,
|
||||
'data_port': int,
|
||||
},
|
||||
'MESSAGES': {
|
||||
'enable_auto_repeat': bool,
|
||||
'adif_log_host': str,
|
||||
'adif_log_port': int,
|
||||
},
|
||||
'GUI':{
|
||||
'QSO_LOGGING': {
|
||||
'enable_adif_udp': bool,
|
||||
'adif_udp_host': str,
|
||||
'adif_udp_port': int,
|
||||
'enable_adif_wavelog': bool,
|
||||
'adif_wavelog_host': str,
|
||||
'adif_wavelog_api_key': str,
|
||||
},
|
||||
|
||||
'GUI': {
|
||||
'auto_run_browser': bool,
|
||||
}
|
||||
}
|
||||
|
@ -104,7 +111,7 @@ class CONFIG:
|
|||
except Exception:
|
||||
self.config_name = "config.ini"
|
||||
|
||||
#self.log.info("[CFG] config init", file=self.config_name)
|
||||
# self.log.info("[CFG] config init", file=self.config_name)
|
||||
|
||||
# check if config file exists
|
||||
self.config_exists()
|
||||
|
@ -167,7 +174,7 @@ class CONFIG:
|
|||
# Handle special setting data type conversion
|
||||
# is_writing means data from a dict being writen to the config file
|
||||
# if False, it means the opposite direction
|
||||
def handle_setting(self, section, setting, value, is_writing = False):
|
||||
def handle_setting(self, section, setting, value, is_writing=False):
|
||||
try:
|
||||
if self.config_types[section][setting] == list:
|
||||
if is_writing:
|
||||
|
@ -181,8 +188,6 @@ class CONFIG:
|
|||
return json.loads(value)
|
||||
return value # Return as-is if already a list
|
||||
|
||||
|
||||
|
||||
elif self.config_types[section][setting] == bool and not is_writing:
|
||||
return self.parser.getboolean(section, setting)
|
||||
|
||||
|
@ -227,12 +232,12 @@ class CONFIG:
|
|||
"""
|
||||
read config file
|
||||
"""
|
||||
#self.log.info("[CFG] reading...")
|
||||
# self.log.info("[CFG] reading...")
|
||||
if not self.config_exists():
|
||||
return False
|
||||
|
||||
# at first just copy the config as read from file
|
||||
result = {s:dict(self.parser.items(s)) for s in self.parser.sections()}
|
||||
result = {s: dict(self.parser.items(s)) for s in self.parser.sections()}
|
||||
|
||||
# handle the special settings
|
||||
for section in result:
|
||||
|
|
|
@ -181,9 +181,9 @@ class DatabaseManagerMessages(DatabaseManager):
|
|||
print(origin_info)
|
||||
if origin_info and "location" in origin_info and origin_info["location"] is not None:
|
||||
print(origin_info["location"])
|
||||
grid = origin_info["location"].get("gridsquare", "----")
|
||||
grid = origin_info["location"].get("gridsquare", "")
|
||||
else:
|
||||
grid = "----"
|
||||
grid = ""
|
||||
|
||||
|
||||
# Extract and adjust the frequency (Hz to MHz)
|
||||
|
|
|
@ -24,7 +24,10 @@ class Modulator:
|
|||
self.freedv_datac4_tx = codec2.open_instance(codec2.FREEDV_MODE.datac4.value)
|
||||
self.freedv_datac13_tx = codec2.open_instance(codec2.FREEDV_MODE.datac13.value)
|
||||
self.freedv_datac14_tx = codec2.open_instance(codec2.FREEDV_MODE.datac14.value)
|
||||
self.data_ofdm_200_tx = codec2.open_instance(codec2.FREEDV_MODE.data_ofdm_200.value)
|
||||
self.data_ofdm_250_tx = codec2.open_instance(codec2.FREEDV_MODE.data_ofdm_250.value)
|
||||
self.data_ofdm_500_tx = codec2.open_instance(codec2.FREEDV_MODE.data_ofdm_500.value)
|
||||
self.data_ofdm_1700_tx = codec2.open_instance(codec2.FREEDV_MODE.data_ofdm_1700.value)
|
||||
self.data_ofdm_2438_tx = codec2.open_instance(codec2.FREEDV_MODE.data_ofdm_2438.value)
|
||||
#self.freedv_qam16c2_tx = codec2.open_instance(codec2.FREEDV_MODE.qam16c2.value)
|
||||
#self.data_qam_2438_tx = codec2.open_instance(codec2.FREEDV_MODE.data_qam_2438.value)
|
||||
|
@ -119,7 +122,10 @@ class Modulator:
|
|||
codec2.FREEDV_MODE.datac4: self.freedv_datac4_tx,
|
||||
codec2.FREEDV_MODE.datac13: self.freedv_datac13_tx,
|
||||
codec2.FREEDV_MODE.datac14: self.freedv_datac14_tx,
|
||||
codec2.FREEDV_MODE.data_ofdm_200: self.data_ofdm_200_tx,
|
||||
codec2.FREEDV_MODE.data_ofdm_250: self.data_ofdm_250_tx,
|
||||
codec2.FREEDV_MODE.data_ofdm_500: self.data_ofdm_500_tx,
|
||||
codec2.FREEDV_MODE.data_ofdm_1700: self.data_ofdm_1700_tx,
|
||||
codec2.FREEDV_MODE.data_ofdm_2438: self.data_ofdm_2438_tx,
|
||||
#codec2.FREEDV_MODE.qam16c2: self.freedv_qam16c2_tx,
|
||||
#codec2.FREEDV_MODE.data_qam_2438: self.freedv_data_qam_2438_tx,
|
||||
|
|
|
@ -35,6 +35,7 @@ import event_manager
|
|||
import structlog
|
||||
from log_handler import setup_logging
|
||||
import adif_udp_logger
|
||||
import wavelog_api_logger
|
||||
|
||||
from message_system_db_manager import DatabaseManager
|
||||
from message_system_db_messages import DatabaseManagerMessages
|
||||
|
@ -1671,8 +1672,14 @@ async def post_freedata_message(request: Request):
|
|||
})
|
||||
async def post_freedata_message_adif_log(message_id: str):
|
||||
adif_output = DatabaseManagerMessages(app.event_manager).get_message_by_id_adif(message_id)
|
||||
|
||||
# if message not found do not send adif as the return then is not valid
|
||||
if not adif_output:
|
||||
return
|
||||
|
||||
# Send the ADIF data via UDP
|
||||
adif_udp_logger.send_adif_qso_data(app.config_manager.read(), adif_output)
|
||||
wavelog_api_logger.send_wavelog_qso_data(app.config_manager.read(), adif_output)
|
||||
return api_response(adif_output)
|
||||
|
||||
@app.patch("/freedata/messages/{message_id}", summary="Update Message by ID", tags=["FreeDATA"], responses={
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import requests
|
||||
import re
|
||||
import structlog
|
||||
|
||||
def send_wavelog_qso_data(config, wavelog_data):
|
||||
"""
|
||||
Sends wavelog QSO data to the specified server via API call.
|
||||
|
||||
Parameters:
|
||||
server_host:port (str)
|
||||
server_api_key (str)
|
||||
wavelog_data (str): wavelog-formatted ADIF QSO data.
|
||||
"""
|
||||
|
||||
log = structlog.get_logger()
|
||||
|
||||
# If False then exit the function
|
||||
wavelog = config['QSO_LOGGING'].get('enable_adif_wavelog', 'False')
|
||||
|
||||
if not wavelog:
|
||||
return # exit as we don't want to log Wavelog
|
||||
|
||||
wavelog_host = config['QSO_LOGGING'].get('adif_wavelog_host', 'http://localhost/')
|
||||
wavelog_api_key = config['QSO_LOGGING'].get('adif_wavelog_api_key', '')
|
||||
|
||||
# check if the last part in the HOST URL from the config is correct
|
||||
if wavelog_host.endswith("/"):
|
||||
url = wavelog_host + "index.php/api/qso"
|
||||
else:
|
||||
url = wavelog_host + "/" + "index.php/api/qso"
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
|
||||
data = {
|
||||
"key": wavelog_api_key,
|
||||
"station_profile_id": "1",
|
||||
"type": "adif",
|
||||
"string": wavelog_data
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, headers=headers, json=data)
|
||||
response.raise_for_status() # Raise an error for bad status codes
|
||||
log.info(f"[CHAT] Wavelog API: {wavelog_data}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
log.warning(f"[WAVELOG ADIF API EXCEPTION]: {e}")
|
|
@ -52,18 +52,25 @@ class FreeDV:
|
|||
|
||||
# Usage example
|
||||
if __name__ == "__main__":
|
||||
MODE = FREEDV_MODE.data_ofdm_2438
|
||||
|
||||
# geht
|
||||
MODE = FREEDV_MODE.data_ofdm_250
|
||||
RX_MODE = FREEDV_MODE.datac4
|
||||
|
||||
# fail
|
||||
#MODE = FREEDV_MODE.datac4
|
||||
#RX_MODE = FREEDV_MODE.data_ofdm_250
|
||||
|
||||
FRAMES = 1
|
||||
|
||||
freedv_instance = FreeDV(MODE, 'config.ini')
|
||||
freedv_rx_instance = FreeDV(RX_MODE, 'config.ini')
|
||||
|
||||
|
||||
|
||||
message = b'A'
|
||||
txbuffer = freedv_instance.modulator.create_burst(MODE, 1, 100, message)
|
||||
message = b'ABC'
|
||||
txbuffer = freedv_instance.modulator.create_burst(MODE, FRAMES, 100, message)
|
||||
freedv_instance.write_to_file(txbuffer, 'ota_audio.raw')
|
||||
txbuffer = np.frombuffer(txbuffer, dtype=np.int16)
|
||||
freedv_instance.demodulate(txbuffer)
|
||||
freedv_rx_instance.demodulate(txbuffer)
|
||||
|
||||
|
||||
# ./src/freedv_data_raw_rx --framesperburst 2 --testframes DATAC0 - /dev/null --vv
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
"""
|
||||
AI-Generated FreeDATA Mode Testing Script by DJ2LS using ChatGPT
|
||||
|
||||
This script tests different FreeDV modes for their ability to modulate and demodulate data.
|
||||
It evaluates the following metrics:
|
||||
- Average audio volume in dB
|
||||
- Max possible audio volume in dB
|
||||
- Peak-to-Average Power Ratio (PAPR)
|
||||
- Frequency spectrum analysis using FFT
|
||||
|
||||
The script runs predefined mode pairs in both transmission and reception directions,
|
||||
and visualizes the results in separate plots.
|
||||
"""
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.append('freedata_server')
|
||||
|
||||
import ctypes
|
||||
import threading
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import pandas as pd
|
||||
from collections import defaultdict
|
||||
from scipy.fftpack import fft
|
||||
from codec2 import open_instance, api, audio_buffer, FREEDV_MODE, resampler
|
||||
import modulator
|
||||
import config
|
||||
import helpers
|
||||
|
||||
|
||||
class FreeDV:
|
||||
def __init__(self, mode, config_file):
|
||||
self.mode = mode
|
||||
self.config = config.CONFIG(config_file)
|
||||
self.modulator = modulator.Modulator(self.config.read())
|
||||
self.freedv = open_instance(self.mode.value)
|
||||
|
||||
def demodulate(self, txbuffer):
|
||||
c2instance = open_instance(self.mode.value)
|
||||
bytes_per_frame = int(api.freedv_get_bits_per_modem_frame(c2instance) / 8)
|
||||
bytes_out = ctypes.create_string_buffer(bytes_per_frame)
|
||||
api.freedv_set_frames_per_burst(c2instance, 1)
|
||||
audiobuffer = audio_buffer(len(txbuffer))
|
||||
nin = api.freedv_nin(c2instance)
|
||||
audiobuffer.push(txbuffer)
|
||||
threading.Event().wait(0.01)
|
||||
|
||||
while audiobuffer.nbuffer >= nin:
|
||||
nbytes = api.freedv_rawdatarx(self.freedv, bytes_out, audiobuffer.buffer.ctypes)
|
||||
rx_status = api.freedv_get_rx_status(self.freedv)
|
||||
nin = api.freedv_nin(self.freedv)
|
||||
audiobuffer.pop(nin)
|
||||
if nbytes == bytes_per_frame:
|
||||
api.freedv_set_sync(self.freedv, 0)
|
||||
return True # Passed
|
||||
|
||||
return False # Failed
|
||||
|
||||
def compute_audio_metrics(self, txbuffer):
|
||||
"""Compute Average Volume in dB, Max Possible Volume, PAPR, and FFT for a given signal."""
|
||||
# Ensure correct dtype and normalize to float range [-1, 1]
|
||||
txbuffer = txbuffer.astype(np.float32) / 32768.0
|
||||
|
||||
avg_volume = np.mean(np.abs(txbuffer))
|
||||
avg_volume_db = 20 * np.log10(avg_volume) if avg_volume > 0 else -np.inf
|
||||
max_possible_volume_db = 20 * np.log10(1.0) # Max possible volume when signal is fully utilized
|
||||
max_val = np.max(np.abs(txbuffer))
|
||||
|
||||
# Prevent division by zero and ensure reasonable values
|
||||
if avg_volume == 0 or max_val == 0:
|
||||
papr = 0
|
||||
else:
|
||||
papr = 10 * np.log10((max_val ** 2) / (avg_volume ** 2))
|
||||
|
||||
# Compute FFT
|
||||
fft_values = np.abs(fft(txbuffer))[:len(txbuffer) // 2]
|
||||
freqs = np.fft.fftfreq(len(txbuffer), d=1 / 8000)[:len(txbuffer) // 2] # Assuming 8 kHz sample rate
|
||||
|
||||
return avg_volume_db, max_possible_volume_db, papr, freqs, fft_values
|
||||
|
||||
def write_to_file(self, txbuffer, filename):
|
||||
with open(filename, 'wb') as f:
|
||||
f.write(txbuffer)
|
||||
|
||||
|
||||
def plot_audio_metrics(avg_volume_per_mode, avg_max_volume_per_mode, avg_papr_per_mode):
|
||||
"""Plot audio metrics in a separate window."""
|
||||
plt.figure(figsize=(10, 5))
|
||||
modes = list(avg_volume_per_mode.keys())
|
||||
volume_values = list(avg_volume_per_mode.values())
|
||||
max_volume_values = list(avg_max_volume_per_mode.values())
|
||||
papr_values = list(avg_papr_per_mode.values())
|
||||
|
||||
plt.plot(modes, volume_values, marker='o', linestyle='-', label='Average Volume (dB)')
|
||||
plt.plot(modes, max_volume_values, marker='x', linestyle='--', label='Max Possible Volume (dB)', color='blue')
|
||||
plt.plot(modes, papr_values, marker='s', linestyle='-', label='Average PAPR (dB)', color='red')
|
||||
plt.ylabel('Volume (dB) / PAPR (dB)')
|
||||
plt.xlabel('Modes')
|
||||
plt.title('Audio Metrics per Mode')
|
||||
plt.legend()
|
||||
plt.xticks(rotation=45, ha='right')
|
||||
plt.pause(0.1)
|
||||
|
||||
|
||||
def plot_fft_per_mode(fft_data):
|
||||
"""Plot FFTs in a separate window."""
|
||||
for mode, (freqs, fft_values) in fft_data.items():
|
||||
plt.figure(figsize=(8, 4))
|
||||
plt.plot(freqs, fft_values, label=f'FFT {mode}')
|
||||
plt.xlabel('Frequency (Hz)')
|
||||
plt.ylabel('Magnitude')
|
||||
plt.title(f'FFT of {mode}')
|
||||
plt.legend()
|
||||
plt.pause(0.1)
|
||||
|
||||
|
||||
def plot_results_summary(results):
|
||||
"""Plot pass/fail results for each mode pair."""
|
||||
mode_pairs = [f"{tx} -> {rx}" for tx, rx, _, _, _, _ in results]
|
||||
pass_fail = [1 if result[2] else -1 for result in results] # Convert True/False to 1/0
|
||||
colors = ['green' if r == 1 else 'red' for r in pass_fail]
|
||||
|
||||
plt.figure(figsize=(10, 5))
|
||||
plt.bar(mode_pairs, pass_fail, color=colors)
|
||||
plt.ylabel('Pass (1) / Fail (0)')
|
||||
plt.xlabel('Mode Pairs')
|
||||
plt.title('Mode Constellation Pass/Fail Summary')
|
||||
plt.xticks(rotation=45, ha='right')
|
||||
plt.ylim(-1, 1) # Ensure bars are properly visible
|
||||
plt.show()
|
||||
|
||||
def test_freedv_mode_pairs(mode_pairs, config_file='config.ini'):
|
||||
results = []
|
||||
fft_data = {}
|
||||
volume_per_mode = {}
|
||||
max_volume_per_mode = {}
|
||||
papr_per_mode = {}
|
||||
|
||||
for tx_mode, rx_mode in mode_pairs:
|
||||
for test_tx, test_rx in [(tx_mode, rx_mode), (rx_mode, tx_mode)]:
|
||||
freedv_tx = FreeDV(test_tx, config_file)
|
||||
freedv_rx = FreeDV(test_rx, config_file)
|
||||
|
||||
message = b'ABC'
|
||||
txbuffer = freedv_tx.modulator.create_burst(test_tx, 1, 100, message)
|
||||
txbuffer = np.frombuffer(txbuffer, dtype=np.int16)
|
||||
|
||||
result = freedv_rx.demodulate(txbuffer)
|
||||
avg_volume_db, max_possible_volume_db, papr, freqs, fft_values = freedv_tx.compute_audio_metrics(txbuffer)
|
||||
results.append((test_tx.name, test_rx.name, result, avg_volume_db, max_possible_volume_db, papr))
|
||||
volume_per_mode[test_tx.name] = avg_volume_db
|
||||
max_volume_per_mode[test_tx.name] = max_possible_volume_db
|
||||
papr_per_mode[test_tx.name] = papr
|
||||
fft_data[test_tx.name] = (freqs, fft_values)
|
||||
|
||||
return results, volume_per_mode, max_volume_per_mode, papr_per_mode, fft_data
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_mode_pairs = [
|
||||
(FREEDV_MODE.datac13, FREEDV_MODE.data_ofdm_200),
|
||||
(FREEDV_MODE.datac14, FREEDV_MODE.datac14),
|
||||
(FREEDV_MODE.datac4, FREEDV_MODE.data_ofdm_250),
|
||||
(FREEDV_MODE.data_ofdm_500, FREEDV_MODE.data_ofdm_500),
|
||||
(FREEDV_MODE.datac0, FREEDV_MODE.datac0),
|
||||
(FREEDV_MODE.datac3, FREEDV_MODE.datac3),
|
||||
(FREEDV_MODE.datac1, FREEDV_MODE.data_ofdm_1700),
|
||||
(FREEDV_MODE.data_ofdm_2438, FREEDV_MODE.data_ofdm_2438),
|
||||
]
|
||||
results, avg_volume_per_mode, avg_max_volume_per_mode, avg_papr_per_mode, fft_data = test_freedv_mode_pairs(
|
||||
test_mode_pairs)
|
||||
plot_audio_metrics(avg_volume_per_mode, avg_max_volume_per_mode, avg_papr_per_mode)
|
||||
plot_fft_per_mode(fft_data)
|
||||
plot_results_summary(results)
|
Loading…
Reference in New Issue