Merge pull request #323 from DJ2LS/ls-arq

Further ARQ improvements + speed chart
This commit is contained in:
DJ2LS 2023-01-16 10:15:51 +01:00 committed by GitHub
commit 3c0484cf46
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 782 additions and 196 deletions

View file

@ -32,8 +32,8 @@
"bootstrap": "^5.2.1", "bootstrap": "^5.2.1",
"bootstrap-icons": "^1.9.1", "bootstrap-icons": "^1.9.1",
"bootswatch": "^5.2.0", "bootswatch": "^5.2.0",
"chart.js": "^3.9.1", "chart.js": "^4.0.0",
"chartjs-plugin-annotation": "^2.0.1", "chartjs-plugin-annotation": "^2.1.2",
"electron-log": "^4.4.8", "electron-log": "^4.4.8",
"electron-updater": "^5.2.1", "electron-updater": "^5.2.1",
"emoji-picker-element": "^1.12.1", "emoji-picker-element": "^1.12.1",

View file

@ -1,5 +1,5 @@
const path = require('path'); const path = require('path');
const {ipcRenderer} = require('electron'); const {ipcRenderer, shell} = require('electron');
const exec = require('child_process').spawn; const exec = require('child_process').spawn;
const sock = require('./sock.js'); const sock = require('./sock.js');
const daemon = require('./daemon.js'); const daemon = require('./daemon.js');
@ -37,8 +37,76 @@ var dbfs_level_raw = 0
// WINDOW LISTENER // WINDOW LISTENER
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
// save frequency event listener
document.getElementById("saveFrequency").addEventListener("click", () => {
var freq = document.getElementById("newFrequency").value;
console.log(freq)
let Data = {
type: "set",
command: "frequency",
frequency: freq,
};
ipcRenderer.send('run-tnc-command', Data);
});
// enter button for input field
document.getElementById("newFrequency").addEventListener("keypress", function(event) {
if (event.key === "Enter") {
event.preventDefault();
document.getElementById("saveFrequency").click();
}
});
// save mode event listener
document.getElementById("saveModePKTUSB").addEventListener("click", () => {
let Data = {
type: "set",
command: "mode",
mode: "PKTUSB",
};
ipcRenderer.send('run-tnc-command', Data);
});
// save mode event listener
document.getElementById("saveModeUSB").addEventListener("click", () => {
let Data = {
type: "set",
command: "mode",
mode: "USB",
};
ipcRenderer.send('run-tnc-command', Data);
});
// save mode event listener
document.getElementById("saveModeLSB").addEventListener("click", () => {
let Data = {
type: "set",
command: "mode",
mode: "LSB",
};
ipcRenderer.send('run-tnc-command', Data);
});
// save mode event listener
document.getElementById("saveModeAM").addEventListener("click", () => {
let Data = {
type: "set",
command: "mode",
mode: "AM",
};
ipcRenderer.send('run-tnc-command', Data);
});
// save mode event listener
document.getElementById("saveModeFM").addEventListener("click", () => {
let Data = {
type: "set",
command: "mode",
mode: "FM",
};
ipcRenderer.send('run-tnc-command', Data);
});
// start stop audio recording event listener // start stop audio recording event listener
document.getElementById("startStopRecording").addEventListener("click", () => { document.getElementById("startStopRecording").addEventListener("click", () => {
@ -221,16 +289,57 @@ document.getElementById('openReceivedFilesFolder').addEventListener('click', ()
if (config.spectrum == 'waterfall') { if (config.spectrum == 'waterfall') {
document.getElementById("waterfall-scatter-switch1").checked = true; document.getElementById("waterfall-scatter-switch1").checked = true;
document.getElementById("waterfall-scatter-switch2").checked = false; document.getElementById("waterfall-scatter-switch2").checked = false;
document.getElementById("scatter").style.visibility = 'hidden'; document.getElementById("waterfall-scatter-switch3").checked = false;
document.getElementById("waterfall").style.visibility = 'visible'; document.getElementById("waterfall").style.visibility = 'visible';
document.getElementById("waterfall").style.height = '100%'; document.getElementById("waterfall").style.height = '100%';
} else { document.getElementById("waterfall").style.display = 'block';
document.getElementById("scatter").style.height = '0px';
document.getElementById("scatter").style.visibility = 'hidden';
document.getElementById("scatter").style.display = 'none';
document.getElementById("chart").style.height = '0px';
document.getElementById("chart").style.visibility = 'hidden';
document.getElementById("chart").style.display = 'none';
} else if (config.spectrum == 'scatter'){
document.getElementById("waterfall-scatter-switch1").checked = false; document.getElementById("waterfall-scatter-switch1").checked = false;
document.getElementById("waterfall-scatter-switch2").checked = true; document.getElementById("waterfall-scatter-switch2").checked = true;
document.getElementById("scatter").style.visibility = 'visible'; document.getElementById("waterfall-scatter-switch3").checked = false;
document.getElementById("waterfall").style.visibility = 'hidden'; document.getElementById("waterfall").style.visibility = 'hidden';
document.getElementById("waterfall").style.height = '0px'; document.getElementById("waterfall").style.height = '0px';
document.getElementById("waterfall").style.display = 'none';
document.getElementById("scatter").style.height = '100%';
document.getElementById("scatter").style.visibility = 'visible';
document.getElementById("scatter").style.display = 'block';
document.getElementById("chart").style.visibility = 'hidden';
document.getElementById("chart").style.height = '0px';
document.getElementById("chart").style.display = 'none';
} else {
document.getElementById("waterfall-scatter-switch1").checked = false;
document.getElementById("waterfall-scatter-switch2").checked = false;
document.getElementById("waterfall-scatter-switch3").checked = true;
document.getElementById("waterfall").style.visibility = 'hidden';
document.getElementById("waterfall").style.height = '0px';
document.getElementById("waterfall").style.display = 'none';
document.getElementById("scatter").style.height = '0px';
document.getElementById("scatter").style.visibility = 'hidden';
document.getElementById("scatter").style.display = 'none';
document.getElementById("chart").style.visibility = 'visible';
document.getElementById("chart").style.height = '100%';
document.getElementById("chart").style.display = 'block';
} }
// radio control element // radio control element
@ -703,21 +812,55 @@ document.getElementById('hamlib_rigctld_stop').addEventListener('click', () => {
// on click waterfall scatter toggle view // on click waterfall scatter toggle view
// waterfall // waterfall
document.getElementById("waterfall-scatter-switch1").addEventListener("click", () => { document.getElementById("waterfall-scatter-switch1").addEventListener("click", () => {
document.getElementById("chart").style.visibility = 'hidden';
document.getElementById("chart").style.display = 'none';
document.getElementById("chart").style.height = '0px';
document.getElementById("scatter").style.height = '0px';
document.getElementById("scatter").style.display = 'none';
document.getElementById("scatter").style.visibility = 'hidden'; document.getElementById("scatter").style.visibility = 'hidden';
document.getElementById("waterfall").style.display = 'block';
document.getElementById("waterfall").style.visibility = 'visible'; document.getElementById("waterfall").style.visibility = 'visible';
document.getElementById("waterfall").style.height = '100%'; document.getElementById("waterfall").style.height = '100%';
config.spectrum = 'waterfall'; config.spectrum = 'waterfall';
fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
}); });
// scatter // scatter
document.getElementById("waterfall-scatter-switch2").addEventListener("click", () => { document.getElementById("waterfall-scatter-switch2").addEventListener("click", () => {
document.getElementById("scatter").style.display = 'block';
document.getElementById("scatter").style.visibility = 'visible'; document.getElementById("scatter").style.visibility = 'visible';
document.getElementById("scatter").style.height = '100%';
document.getElementById("waterfall").style.visibility = 'hidden'; document.getElementById("waterfall").style.visibility = 'hidden';
document.getElementById("waterfall").style.height = '0px'; document.getElementById("waterfall").style.height = '0px';
document.getElementById("waterfall").style.display = 'none';
document.getElementById("chart").style.visibility = 'hidden';
document.getElementById("chart").style.height = '0px';
document.getElementById("chart").style.display = 'none';
config.spectrum = 'scatter'; config.spectrum = 'scatter';
fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
}); });
// chart
document.getElementById("waterfall-scatter-switch3").addEventListener("click", () => {
document.getElementById("waterfall").style.visibility = 'hidden';
document.getElementById("waterfall").style.height = '0px';
document.getElementById("waterfall").style.display = 'none';
document.getElementById("scatter").style.height = '0px';
document.getElementById("scatter").style.visibility = 'hidden';
document.getElementById("scatter").style.display = 'none';
document.getElementById("chart").style.height = '100%';
document.getElementById("chart").style.display = 'block';
document.getElementById("chart").style.visibility = 'visible';
config.spectrum = 'chart';
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
});
// on click remote tnc toggle view // on click remote tnc toggle view
@ -1008,6 +1151,11 @@ document.getElementById('hamlib_rigctld_stop').addEventListener('click', () => {
sock.stopBeacon(); sock.stopBeacon();
}); });
// Explorer button clicked
document.getElementById("openExplorer").addEventListener("click", () => {
shell.openExternal('https://explorer.freedata.app/?myCall=' + document.getElementById("myCall").value);
});
// startTNC button clicked // startTNC button clicked
document.getElementById("startTNC").addEventListener("click", () => { document.getElementById("startTNC").addEventListener("click", () => {
@ -1332,28 +1480,18 @@ ipcRenderer.on('action-update-tnc-state', (event, arg) => {
document.title = documentTitle[0] + 'Call: ' + arg.mycallsign; document.title = documentTitle[0] + 'Call: ' + arg.mycallsign;
} }
// update mygrid information with data from tnc
// TOE TIME OF EXECUTION --> How many time needs a command to be executed until data arrives if (typeof(arg.mygrid) !== 'undefined') {
// deactivated this feature, beacuse its useless at this time. maybe it is getting more interesting, if we are working via network document.getElementById("myGrid").value = arg.mygrid;
// but for this we need to find a nice place for this on the screen
/*
if (typeof(arg.toe) == 'undefined') {
var toe = 0
} else {
var toe = arg.toe
} }
document.getElementById("toe").innerHTML = toe + ' ms'
*/
// DATA STATE // DATA STATE
global.rxBufferLengthTnc = arg.rx_buffer_length global.rxBufferLengthTnc = arg.rx_buffer_length
// SCATTER DIAGRAM PLOTTING
//global.myChart.destroy();
//console.log(arg.scatter.length) // START OF SCATTER CHART
const config = { const scatterConfig = {
plugins: { plugins: {
legend: { legend: {
display: false, display: false,
@ -1407,16 +1545,12 @@ ipcRenderer.on('action-update-tnc-state', (event, arg) => {
} }
} }
} }
var scatterData = arg.scatter
var newScatterData = {
var data = arg.scatter
var newdata = {
datasets: [{ datasets: [{
//label: 'constellation diagram', //label: 'constellation diagram',
data: data, data: scatterData,
options: config, options: scatterConfig,
backgroundColor: 'rgb(255, 99, 132)' backgroundColor: 'rgb(255, 99, 132)'
}], }],
}; };
@ -1426,25 +1560,122 @@ ipcRenderer.on('action-update-tnc-state', (event, arg) => {
} else { } else {
var scatterSize = arg.scatter.length; var scatterSize = arg.scatter.length;
} }
if (global.data != newdata && scatterSize > 0) {
try { if (global.scatterData != newScatterData && scatterSize > 0) {
global.myChart.destroy(); global.scatterData = newScatterData;
} catch (e) {
// myChart not yet created if (typeof(global.scatterChart) == 'undefined') {
console.log(e); var scatterCtx = document.getElementById('scatter').getContext('2d');
global.scatterChart = new Chart(scatterCtx, {
type: 'scatter',
data: global.scatterData,
options: scatterConfig
});
} else {
global.scatterChart.data = global.scatterData;
global.scatterChart.update();
}
}
// END OF SCATTER CHART
// START OF SPEED CHART
var speedDataTime = []
if (typeof(arg.speed_list) == 'undefined') {
var speed_listSize = 0;
} else {
var speed_listSize = arg.speed_list.length;
}
for (var i=0; i < speed_listSize; i++) {
var timestamp = arg.speed_list[i].timestamp * 1000
var h = new Date(timestamp).getHours();
var m = new Date(timestamp).getMinutes();
var s = new Date(timestamp).getSeconds();
var time = h + ':' + m + ':' + s;
speedDataTime.push(time)
}
var speedDataBpm = []
for (var i=0; i < speed_listSize; i++) {
speedDataBpm.push(arg.speed_list[i].bpm)
}
var speedDataSnr = []
for (var i=0; i < speed_listSize; i++) {
speedDataSnr.push(arg.speed_list[i].snr)
}
var speedChartConfig = {
type: 'line',
};
var newSpeedData = {
labels: speedDataTime,
datasets: [
{
type: 'line',
label: 'SNR[dB]',
data: speedDataSnr,
borderColor: 'rgb(255, 99, 132, 1.0)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
order: 1,
yAxisID: 'SNR',
},
{
type: 'bar',
label: 'Speed[bpm]',
data: speedDataBpm,
borderColor: 'rgb(120, 100, 120, 1.0)',
backgroundColor: 'rgba(120, 100, 120, 0.2)',
order: 0,
yAxisID: 'SPEED',
}
],
};
var speedChartOptions = {
responsive: true,
animations: true,
cubicInterpolationMode: 'monotone',
tension: 0.4,
scales: {
SNR:{
type: 'linear',
ticks: { beginAtZero: true, color: 'rgb(255, 99, 132)' },
position: 'right',
},
SPEED :{
type: 'linear',
ticks: { beginAtZero: true, color: 'rgb(120, 100, 120)' },
position: 'left',
grid: {
drawOnChartArea: false, // only want the grid lines for one axis to show up
},
},
x: { ticks: { beginAtZero: true } },
}
} }
global.data = newdata; if (typeof(global.speedChart) == 'undefined') {
var speedCtx = document.getElementById('chart').getContext('2d');
global.speedChart = new Chart(speedCtx, {
var ctx = document.getElementById('scatter').getContext('2d'); data: newSpeedData,
global.myChart = new Chart(ctx, { options: speedChartOptions
type: 'scatter',
data: global.data,
options: config
}); });
} else {
if(speedDataSnr.length > 0){
global.speedChart.data = newSpeedData;
global.speedChart.update();
}
} }
// END OF SPEED CHART
// PTT STATE // PTT STATE
if (arg.ptt_state == 'True') { if (arg.ptt_state == 'True') {
document.getElementById("ptt_state").className = "btn btn-sm btn-danger"; document.getElementById("ptt_state").className = "btn btn-sm btn-danger";
@ -1566,7 +1797,10 @@ ipcRenderer.on('action-update-tnc-state', (event, arg) => {
} }
// SET FREQUENCY // SET FREQUENCY
document.getElementById("frequency").innerHTML = arg.frequency; // https://stackoverflow.com/a/2901298
var freq = arg.frequency.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
document.getElementById("frequency").innerHTML = freq;
//document.getElementById("newFrequency").value = arg.frequency;
// SET MODE // SET MODE
document.getElementById("mode").innerHTML = arg.mode; document.getElementById("mode").innerHTML = arg.mode;
@ -1589,7 +1823,30 @@ ipcRenderer.on('action-update-tnc-state', (event, arg) => {
var arq_bytes_per_minute_compressed = Math.round(arg.arq_bytes_per_minute * arg.arq_compression_factor); var arq_bytes_per_minute_compressed = Math.round(arg.arq_bytes_per_minute * arg.arq_compression_factor);
} }
document.getElementById("bytes_per_min_compressed").innerHTML = arq_bytes_per_minute_compressed; document.getElementById("bytes_per_min_compressed").innerHTML = arq_bytes_per_minute_compressed;
// SET TIME LEFT UNTIL FINIHED
if (typeof(arg.arq_seconds_until_finish) == 'undefined') {
var time_left = 0;
} else {
var arq_seconds_until_finish = arg.arq_seconds_until_finish
var hours = Math.floor(arq_seconds_until_finish / 3600);
var minutes = Math.floor((arq_seconds_until_finish % 3600) / 60 );
var seconds = arq_seconds_until_finish % 60;
if(hours < 0) {
hours = 0;
}
if(minutes < 0) {
minutes = 0;
}
if(seconds < 0) {
seconds = 0;
}
var time_left = "time left: ~ "+ minutes + "min" + " " + seconds + "s";
}
document.getElementById("transmission_timeleft").innerHTML = time_left;
// SET SPEED LEVEL // SET SPEED LEVEL
@ -1608,6 +1865,7 @@ ipcRenderer.on('action-update-tnc-state', (event, arg) => {
if(arg.speed_level >= 4) { if(arg.speed_level >= 4) {
document.getElementById("speed_level").className = "bi bi-reception-4"; document.getElementById("speed_level").className = "bi bi-reception-4";
} }
@ -2109,8 +2367,15 @@ ipcRenderer.on('run-tnc-command', (event, arg) => {
} }
if (arg.command == 'send_test_frame') { if (arg.command == 'send_test_frame') {
sock.sendTestFrame(); sock.sendTestFrame();
} }
if (arg.command == 'frequency') {
sock.set_frequency(arg.frequency);
}
if (arg.command == 'mode') {
sock.set_mode(arg.mode);
}
}); });

View file

@ -196,6 +196,7 @@ client.on('data', function(socketdata) {
let Data = { let Data = {
mycallsign: data['mycallsign'], mycallsign: data['mycallsign'],
mygrid: data['mygrid'],
ptt_state: data['ptt_state'], ptt_state: data['ptt_state'],
busy_state: data['tnc_state'], busy_state: data['tnc_state'],
arq_state: data['arq_state'], arq_state: data['arq_state'],
@ -221,6 +222,7 @@ client.on('data', function(socketdata) {
arq_rx_n_current_arq_frame: data['arq_rx_n_current_arq_frame'], arq_rx_n_current_arq_frame: data['arq_rx_n_current_arq_frame'],
arq_n_arq_frames_per_data_frame: data['arq_n_arq_frames_per_data_frame'], arq_n_arq_frames_per_data_frame: data['arq_n_arq_frames_per_data_frame'],
arq_bytes_per_minute: data['arq_bytes_per_minute'], arq_bytes_per_minute: data['arq_bytes_per_minute'],
arq_seconds_until_finish: data['arq_seconds_until_finish'],
arq_compression_factor: data['arq_compression_factor'], arq_compression_factor: data['arq_compression_factor'],
total_bytes: data['total_bytes'], total_bytes: data['total_bytes'],
arq_transmission_percent: data['arq_transmission_percent'], arq_transmission_percent: data['arq_transmission_percent'],
@ -229,6 +231,8 @@ client.on('data', function(socketdata) {
hamlib_status: data['hamlib_status'], hamlib_status: data['hamlib_status'],
listen: data['listen'], listen: data['listen'],
audio_recording: data['audio_recording'], audio_recording: data['audio_recording'],
speed_list: data['speed_list'],
//speed_table: [{"bpm" : 5200, "snr": -3, "timestamp":1673555399},{"bpm" : 2315, "snr": 12, "timestamp":1673555500}],
}; };
ipcRenderer.send('request-update-tnc-state', Data); ipcRenderer.send('request-update-tnc-state', Data);
@ -597,6 +601,19 @@ exports.record_audio = function() {
writeTncCommand(command) writeTncCommand(command)
} }
// SET FREQUENCY
exports.set_frequency = function(frequency) {
command = '{"type" : "set", "command" : "frequency", "frequency": '+ frequency +'}'
writeTncCommand(command)
}
// SET MODE
exports.set_mode = function(mode) {
command = '{"type" : "set", "command" : "mode", "mode": "'+ mode +'"}'
console.log(command)
writeTncCommand(command)
}
ipcRenderer.on('action-update-tnc-ip', (event, arg) => { ipcRenderer.on('action-update-tnc-ip', (event, arg) => {
client.destroy(); client.destroy();
let Data = { let Data = {

View file

@ -44,6 +44,7 @@
<button class="btn btn-sm btn-danger" id="stop_transmission_connection" type="button"> <i class="bi bi-x-octagon-fill" style="font-size: 1rem; color: white;"></i> STOP </button> <button class="btn btn-sm btn-danger" id="stop_transmission_connection" type="button"> <i class="bi bi-x-octagon-fill" style="font-size: 1rem; color: white;"></i> STOP </button>
</div> </div>
<div class="btn-toolbar" role="toolbar"> <div class="btn-toolbar" role="toolbar">
<button class="btn btn-sm btn-primary me-4 position-relative" id="openExplorer" type="button" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="View explorer map"> <strong>Explorer</strong> <i class="bi bi-pin-map-fill" style="font-size: 1rem; color: white;"></i></button>
<button class="btn btn-sm btn-primary me-4 position-relative" id="openRFChat" type="button" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="Open the HF chat module. This is currently just a test and not finished, yet!"> <strong>RF Chat</strong> <i class="bi bi-chat-left-text-fill" style="font-size: 1rem; color: white;"></i> <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">.</span> </button> <span data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="View the received files. This is currently under development!"> <button class="btn btn-sm btn-primary me-4 position-relative" id="openRFChat" type="button" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="Open the HF chat module. This is currently just a test and not finished, yet!"> <strong>RF Chat</strong> <i class="bi bi-chat-left-text-fill" style="font-size: 1rem; color: white;"></i> <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">.</span> </button> <span data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="View the received files. This is currently under development!">
<!-- <!--
@ -837,37 +838,19 @@
<div class="card-header p-1"> <div class="card-header p-1">
<div class="btn-group btn-group-sm" role="group" aria-label="waterfall-scatter-switch toggle button group"> <div class="btn-group btn-group-sm" role="group" aria-label="waterfall-scatter-switch toggle button group">
<input type="radio" class="btn-check" name="waterfall-scatter-switch" id="waterfall-scatter-switch1" autocomplete="off" checked> <input type="radio" class="btn-check" name="waterfall-scatter-switch" id="waterfall-scatter-switch1" autocomplete="off" checked>
<label class="btn btn-sm btn-outline-secondary" for="waterfall-scatter-switch1"><strong>WATERFALL</strong> </label> <label class="btn btn-sm btn-outline-secondary" for="waterfall-scatter-switch1"><strong><i class="bi bi-water"></i></strong> </label>
<input type="radio" class="btn-check" name="waterfall-scatter-switch" id="waterfall-scatter-switch2" autocomplete="off"> <input type="radio" class="btn-check" name="waterfall-scatter-switch" id="waterfall-scatter-switch2" autocomplete="off">
<label class="btn btn-sm btn-outline-secondary" for="waterfall-scatter-switch2"><strong>SCATTER</strong> </label> <label class="btn btn-sm btn-outline-secondary" for="waterfall-scatter-switch2"><strong><i class="bi bi-border-outer"></i></strong> </label>
<input type="radio" class="btn-check" name="waterfall-scatter-switch" id="waterfall-scatter-switch3" autocomplete="off">
<label class="btn btn-sm btn-outline-secondary" for="waterfall-scatter-switch3"><strong><i class="bi bi-graph-up-arrow"></i></strong> </label>
</div> </div>
<button class="btn btn-sm btn-secondary" id="channel_busy" type="button" data-bs-placement="top" data-bs-toggle="tooltip" data-bs-html="true" title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>">busy</button> <button class="btn btn-sm btn-secondary" id="channel_busy" type="button" data-bs-placement="top" data-bs-toggle="tooltip" data-bs-html="true" title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>">busy</button>
</div> </div>
<div class="card-body p-1" style="height: 200px"> <div class="card-body p-1" style="height: 200px">
<!-- TEST FOR WATERFALL OVERLAY
<div class="opacity-100 w-100 h-100 p-0 m-0 position-absolute" style="height: 190px;z-index: 10">
<div class="row m-0 p-0 w-100 h-100">
<div class="col m-0 p-0 col-3 ">
-
</div>
<div class="col border border-danger m-0 p-0 col-2">
1800Hz
</div>
<div class="col border border-danger m-0 p-0" style="width: 190px;">
500Hz
</div>
<div class="col border border-danger m-0 p-0 col-2">
-
</div>
<div class="col m-0 p-0 col-3">
-
</div>
</div>
</div>
-->
<!--278px--> <!--278px-->
<canvas id="waterfall" style="position: relative; z-index: 2;"></canvas> <canvas id="waterfall" style="position: relative; z-index: 2; transform: translateZ(0);"></canvas>
<canvas id="scatter" style="position: relative; z-index: 1;"></canvas> <canvas id="scatter" style="position: relative; z-index: 1; transform: translateZ(0);"></canvas>
<canvas id="chart" style="position: relative; z-index: 1; transform: translateZ(0);"></canvas>
</div> </div>
</div> </div>
</div> </div>
@ -1066,28 +1049,64 @@
<nav class="navbar fixed-bottom navbar-light bg-light"> <nav class="navbar fixed-bottom navbar-light bg-light">
<div class="container-fluid"> <div class="container-fluid">
<div class="btn-toolbar" role="toolbar"> <div class="btn-toolbar" role="toolbar">
<div class="btn-group btn-group-sm me-2" role="group"> <div class="btn-group btn-group-sm me-1" role="group">
<button class="btn btn-sm btn-secondary" id="ptt_state" type="button" data-bs-placement="top" data-bs-toggle="tooltip" data-bs-html="true" title="PTT state:<strong class='text-success'>RECEIVING</strong> / <strong class='text-danger'>TRANSMITTING</strong>"> <i class="bi bi-broadcast-pin" style="font-size: 0.8rem; color: white;"></i> </button> <button class="btn btn-sm btn-secondary" id="ptt_state" type="button" data-bs-placement="top" data-bs-toggle="tooltip" data-bs-html="true" title="PTT state:<strong class='text-success'>RECEIVING</strong> / <strong class='text-danger'>TRANSMITTING</strong>"> <i class="bi bi-broadcast-pin" style="font-size: 0.8rem; color: white;"></i> </button>
</div> </div>
<div class="btn-group btn-group-sm me-2" role="group"> <div class="btn-group btn-group-sm me-1" role="group">
<button class="btn btn-sm btn-secondary" id="busy_state" type="button" data-bs-placement="top" data-bs-toggle="tooltip" data-bs-html="true" title="TNC busy state: <strong class='text-success'>IDLE</strong> / <strong class='text-danger'>BUSY</strong>"> <i class="bi bi-cpu" style="font-size: 0.8rem; color: white;"></i> </button> <button class="btn btn-sm btn-secondary" id="busy_state" type="button" data-bs-placement="top" data-bs-toggle="tooltip" data-bs-html="true" title="TNC busy state: <strong class='text-success'>IDLE</strong> / <strong class='text-danger'>BUSY</strong>"> <i class="bi bi-cpu" style="font-size: 0.8rem; color: white;"></i> </button>
</div> </div>
<div class="btn-group btn-group-sm me-2" role="group"> <div class="btn-group btn-group-sm me-1" role="group">
<button class="btn btn-sm btn-secondary" id="arq_session" type="button" data-bs-placement="top" data-bs-toggle="tooltip" data-bs-html="true" title="ARQ SESSION state: <strong class='text-warning'>OPEN</strong>"> <i class="bi bi-arrow-left-right" style="font-size: 0.8rem; color: white;"></i> </button> <button class="btn btn-sm btn-secondary" id="arq_session" type="button" data-bs-placement="top" data-bs-toggle="tooltip" data-bs-html="true" title="ARQ SESSION state: <strong class='text-warning'>OPEN</strong>"> <i class="bi bi-arrow-left-right" style="font-size: 0.8rem; color: white;"></i> </button>
</div> </div>
<div class="btn-group btn-group-sm me-2" role="group"> <div class="btn-group btn-group-sm me-1" role="group">
<button class="btn btn-sm btn-secondary" id="arq_state" type="button" data-bs-placement="top" data-bs-toggle="tooltip" data-bs-html="true" title="DATA-CHANNEL state: <strong class='text-warning'>OPEN</strong>"> <i class="bi bi-file-earmark-binary" style="font-size: 0.8rem; color: white;"></i> </button> <button class="btn btn-sm btn-secondary" id="arq_state" type="button" data-bs-placement="top" data-bs-toggle="tooltip" data-bs-html="true" title="DATA-CHANNEL state: <strong class='text-warning'>OPEN</strong>"> <i class="bi bi-file-earmark-binary" style="font-size: 0.8rem; color: white;"></i> </button>
</div> </div>
<div class="btn-group btn-group-sm me-2" role="group"> <div class="btn-group btn-group-sm me-1" role="group">
<button class="btn btn-sm btn-secondary" id="rigctld_state" type="button" data-bs-placement="top" data-bs-toggle="tooltip" data-bs-html="true" title="rigctld state: <strong class='text-success'>CONNECTED</strong> / <strong class='text-secondary'>UNKNOWN</strong>"> <i class="bi bi-usb-symbol" style="font-size: 0.8rem; color: white;"></i> </button> <button class="btn btn-sm btn-secondary" id="rigctld_state" type="button" data-bs-placement="top" data-bs-toggle="tooltip" data-bs-html="true" title="rigctld state: <strong class='text-success'>CONNECTED</strong> / <strong class='text-secondary'>UNKNOWN</strong>"> <i class="bi bi-usb-symbol" style="font-size: 0.8rem; color: white;"></i> </button>
</div> </div>
</div> </div>
<div class="container-fluid p-0" style="width:15rem"> <div class="container-fluid p-0" style="width:20rem">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<!--<span class="input-group-text" id="basic-addon1"><strong>Freq</strong></span>--><span class="input-group-text" id="frequency">---</span>
<!--<span class="input-group-text" id="basic-addon1"><strong>Mode</strong></span>--><span class="input-group-text" id="mode">---</span> <div class="btn-group dropup me-1">
<!--<span class="input-group-text" id="basic-addon1"><strong>BW</strong></span>--><span class="input-group-text" id="bandwidth">---</span> </div> <button type="button" class="btn btn-sm btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false" id="frequency">
---
</button>
<form class="dropdown-menu p-2">
<div class="input-group input-group-sm">
<input type="text" class="form-control" style="max-width: 6rem;" placeholder="7063000" pattern="[0-9]*" id="newFrequency" maxlength="11" aria-label="Input group" aria-describedby="btnGroupAddon">
<span class="input-group-text">Hz</span>
<button class="btn btn-sm btn-success" id="saveFrequency" type="button" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="save frequency"> <i class="bi bi-check-lg" style="font-size: 0.8rem; color: white;"></i> </button>
</div>
</form>
</div>
<div class="btn-group dropup me-1">
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false" id="mode">
---
</button>
<form class="dropdown-menu p-2">
<button type="button" class="btn btn-sm btn-secondary" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="set FM" id="saveModeFM">FM</button>
<button type="button" class="btn btn-sm btn-secondary" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="set AM" type="button" id="saveModeAM">AM</button>
<button type="button" class="btn btn-sm btn-secondary" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="set LSB" type="button" id="saveModeLSB">LSB</button>
<hr>
<button type="button" class="btn btn-sm btn-secondary" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="set USB" type="button" id="saveModeUSB">USB</button>
<button type="button" class="btn btn-sm btn-secondary" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="set PKTUSB" type="button" id="saveModePKTUSB">PKTUSB</button>
</form>
</div>
<div class="btn-group dropup">
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false" id="bandwidth">
---
</button>
<form class="dropdown-menu p-2">
<div class="input-group input-group-sm">
...soon...
</div>
</form>
</div>
</div>
</div> </div>
<div class="container-fluid p-0" style="width:12rem"> <div class="container-fluid p-0" style="width:12rem">
<div class="input-group input-group-sm"> <span class="input-group-text" id="basic-addon1"><i class="bi bi-speedometer2" style="font-size: 1rem; color: black;"></i></span> <span class="input-group-text" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="actual speed level"> <div class="input-group input-group-sm"> <span class="input-group-text" id="basic-addon1"><i class="bi bi-speedometer2" style="font-size: 1rem; color: black;"></i></span> <span class="input-group-text" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="actual speed level">
@ -1100,6 +1119,7 @@
<div class="progress" style="height: 30px;"> <div class="progress" style="height: 30px;">
<div class="progress-bar progress-bar-striped bg-primary" id="transmission_progress" role="progressbar" style="width: 0%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div> <div class="progress-bar progress-bar-striped bg-primary" id="transmission_progress" role="progressbar" style="width: 0%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
<!--<p class="justify-content-center d-flex position-absolute w-100">PROGRESS</p>--> <!--<p class="justify-content-center d-flex position-absolute w-100">PROGRESS</p>-->
<p class="justify-content-center mt-2 d-flex position-absolute w-100" id="transmission_timeleft">---</p>
</div> </div>
</div> </div>
</div> </div>
@ -1107,8 +1127,8 @@
<!-- bootstrap --> <!-- bootstrap -->
<script src="../node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="../node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<!-- chart.js --> <!-- chart.js -->
<script src="../node_modules/chart.js/dist/chart.min.js"></script> <script src="../node_modules/chart.js/dist/chart.umd.js"></script>
<script src="../node_modules/chartjs-plugin-annotation/dist/chartjs-plugin-annotation.min.js"></script> <!--<script src="../node_modules/chartjs-plugin-annotation/dist/chartjs-plugin-annotation.min.js"></script>-->
<!--<script src="../ui.js"></script>--> <!--<script src="../ui.js"></script>-->
<!-- WATERFALL --> <!-- WATERFALL -->
<script src="waterfall/colormap.js"></script> <script src="waterfall/colormap.js"></script>
@ -1295,4 +1315,4 @@
</div> </div>
</body> </body>
</html> </html>

View file

@ -8,6 +8,7 @@ body {
/*Progress bars with centered text*/ /*Progress bars with centered text*/
.progress { .progress {
position: relative; position: relative;
transform: translateZ(0);
} }
.progress span { .progress span {

View file

@ -64,6 +64,7 @@ class DATA:
self.transmission_uuid = "" self.transmission_uuid = ""
self.burst_last_received = 0.0 # time of last "live sign" of a burst
self.data_channel_last_received = 0.0 # time of last "live sign" of a frame self.data_channel_last_received = 0.0 # time of last "live sign" of a frame
self.burst_ack_snr = 0 # SNR from received burst ack frames self.burst_ack_snr = 0 # SNR from received burst ack frames
@ -397,7 +398,8 @@ class DATA:
:param repeat_delay: Delay time before sending repeat frame, defaults to 0 :param repeat_delay: Delay time before sending repeat frame, defaults to 0
:type repeat_delay: int, optional :type repeat_delay: int, optional
""" """
self.log.debug("[TNC] enqueue_frame_for_tx", c2_mode=FREEDV_MODE(c2_mode).name) frame_type = FR_TYPE(int.from_bytes(frame_to_tx[0][:1], byteorder="big")).name
self.log.debug("[TNC] enqueue_frame_for_tx", c2_mode=FREEDV_MODE(c2_mode).name, data=frame_to_tx, type=frame_type)
# Set the TRANSMITTING flag before adding an object to the transmit queue # Set the TRANSMITTING flag before adding an object to the transmit queue
# TODO: This is not that nice, we could improve this somehow # TODO: This is not that nice, we could improve this somehow
@ -470,9 +472,17 @@ class DATA:
ack_frame[2:3] = helpers.snr_to_bytes(snr) ack_frame[2:3] = helpers.snr_to_bytes(snr)
ack_frame[3:4] = bytes([int(self.speed_level)]) ack_frame[3:4] = bytes([int(self.speed_level)])
# wait while timeout not reached and our busy state is busy
channel_busy_timeout = time.time() + 5
while static.CHANNEL_BUSY and time.time() < channel_busy_timeout:
threading.Event().wait(0.01)
# Transmit frame # Transmit frame
self.enqueue_frame_for_tx([ack_frame], c2_mode=FREEDV_MODE.sig1.value) self.enqueue_frame_for_tx([ack_frame], c2_mode=FREEDV_MODE.sig1.value)
# reset burst timeout in case we had to wait too long
self.burst_last_received = time.time()
def send_data_ack_frame(self, snr) -> None: def send_data_ack_frame(self, snr) -> None:
"""Build and send ACK frame for received DATA frame""" """Build and send ACK frame for received DATA frame"""
@ -485,11 +495,19 @@ class DATA:
# ack_frame[7:8] = bytes([int(snr)]) # ack_frame[7:8] = bytes([int(snr)])
# ack_frame[8:9] = bytes([int(self.speed_level)]) # ack_frame[8:9] = bytes([int(self.speed_level)])
# wait while timeout not reached and our busy state is busy
channel_busy_timeout = time.time() + 5
while static.CHANNEL_BUSY and time.time() < channel_busy_timeout:
threading.Event().wait(0.01)
# Transmit frame # Transmit frame
# TODO: Do we have to send , self.send_ident_frame(False) ? # TODO: Do we have to send , self.send_ident_frame(False) ?
# self.enqueue_frame_for_tx([ack_frame, self.send_ident_frame(False)], c2_mode=FREEDV_MODE.sig1.value, copies=3, repeat_delay=0) # self.enqueue_frame_for_tx([ack_frame, self.send_ident_frame(False)], c2_mode=FREEDV_MODE.sig1.value, copies=3, repeat_delay=0)
self.enqueue_frame_for_tx([ack_frame], c2_mode=FREEDV_MODE.sig1.value, copies=6, repeat_delay=0) self.enqueue_frame_for_tx([ack_frame], c2_mode=FREEDV_MODE.sig1.value, copies=6, repeat_delay=0)
# reset burst timeout in case we had to wait too long
self.burst_last_received = time.time()
def send_retransmit_request_frame(self, freedv) -> None: def send_retransmit_request_frame(self, freedv) -> None:
# check where a None is in our burst buffer and do frame+1, because lists start at 0 # check where a None is in our burst buffer and do frame+1, because lists start at 0
# FIXME: Check to see if there's a `frame - 1` in the receive portion. Remove both if there is. # FIXME: Check to see if there's a `frame - 1` in the receive portion. Remove both if there is.
@ -532,7 +550,15 @@ class DATA:
# TRANSMIT NACK FRAME FOR BURST # TRANSMIT NACK FRAME FOR BURST
# TODO: Do we have to send ident frame? # TODO: Do we have to send ident frame?
# self.enqueue_frame_for_tx([ack_frame, self.send_ident_frame(False)], c2_mode=FREEDV_MODE.sig1.value, copies=3, repeat_delay=0) # self.enqueue_frame_for_tx([ack_frame, self.send_ident_frame(False)], c2_mode=FREEDV_MODE.sig1.value, copies=3, repeat_delay=0)
# wait while timeout not reached and our busy state is busy
channel_busy_timeout = time.time() + 5
while static.CHANNEL_BUSY and time.time() < channel_busy_timeout:
threading.Event().wait(0.01)
self.enqueue_frame_for_tx([nack_frame], c2_mode=FREEDV_MODE.sig1.value, copies=6, repeat_delay=0) self.enqueue_frame_for_tx([nack_frame], c2_mode=FREEDV_MODE.sig1.value, copies=6, repeat_delay=0)
# reset burst timeout in case we had to wait too long
self.burst_last_received = time.time()
def send_burst_nack_frame_watchdog(self, snr: bytes) -> None: def send_burst_nack_frame_watchdog(self, snr: bytes) -> None:
"""Build and send NACK frame for watchdog timeout""" """Build and send NACK frame for watchdog timeout"""
@ -549,8 +575,15 @@ class DATA:
nack_frame[2:3] = helpers.snr_to_bytes(snr) nack_frame[2:3] = helpers.snr_to_bytes(snr)
nack_frame[3:4] = bytes([int(self.speed_level)]) nack_frame[3:4] = bytes([int(self.speed_level)])
# wait while timeout not reached and our busy state is busy
channel_busy_timeout = time.time() + 5
while static.CHANNEL_BUSY and time.time() < channel_busy_timeout:
threading.Event().wait(0.01)
# TRANSMIT NACK FRAME FOR BURST # TRANSMIT NACK FRAME FOR BURST
self.enqueue_frame_for_tx([nack_frame], c2_mode=FREEDV_MODE.sig1.value, copies=1, repeat_delay=0) self.enqueue_frame_for_tx([nack_frame], c2_mode=FREEDV_MODE.sig1.value, copies=1, repeat_delay=0)
# reset burst timeout in case we had to wait too long
self.burst_last_received = time.time()
def send_disconnect_frame(self) -> None: def send_disconnect_frame(self) -> None:
"""Build and send a disconnect frame""" """Build and send a disconnect frame"""
@ -563,6 +596,12 @@ class DATA:
# TODO: We need to add the ident frame feature with a seperate PR after publishing latest protocol # TODO: We need to add the ident frame feature with a seperate PR after publishing latest protocol
# TODO: We need to wait some time between last arq related signalling frame and ident frame # TODO: We need to wait some time between last arq related signalling frame and ident frame
# TODO: Maybe about 500ms - 1500ms to avoid confusion and too much PTT toggles # TODO: Maybe about 500ms - 1500ms to avoid confusion and too much PTT toggles
# wait while timeout not reached and our busy state is busy
channel_busy_timeout = time.time() + 5
while static.CHANNEL_BUSY and time.time() < channel_busy_timeout:
threading.Event().wait(0.01)
self.enqueue_frame_for_tx([disconnection_frame], c2_mode=FREEDV_MODE.sig0.value, copies=6, repeat_delay=0) self.enqueue_frame_for_tx([disconnection_frame], c2_mode=FREEDV_MODE.sig0.value, copies=6, repeat_delay=0)
def arq_data_received( def arq_data_received(
@ -602,6 +641,7 @@ class DATA:
# Update data_channel timestamp # Update data_channel timestamp
self.data_channel_last_received = int(time.time()) self.data_channel_last_received = int(time.time())
self.burst_last_received = int(time.time())
# Extract some important data from the frame # Extract some important data from the frame
# Get sequence number of burst frame # Get sequence number of burst frame
@ -641,6 +681,26 @@ class DATA:
# static.RX_FRAME_BUFFER += static.RX_BURST_BUFFER[i] # static.RX_FRAME_BUFFER += static.RX_BURST_BUFFER[i]
temp_burst_buffer += bytes(value) # type: ignore temp_burst_buffer += bytes(value) # type: ignore
# TODO: Needs to be removed as soon as mode error is fixed
# catch possible modem error which leads into false byteorder
# modem possibly decodes too late - data then is pushed to buffer
# which leads into wrong byteorder
# Lets put this in try/except so we are not crashing tnc as its hihgly experimental
# This might only work for datac1 and datac3
try:
#area_of_interest = (modem.get_bytes_per_frame(self.mode_list[speed_level] - 1) -3) * 2
if static.RX_FRAME_BUFFER.endswith(temp_burst_buffer[:246]) and len(temp_burst_buffer) >= 246:
self.log.warning(
"[TNC] ARQ | RX | wrong byteorder received - dropping data"
)
# we need to run a return here, so we are not sending an ACK
return
except Exception as e:
self.log.warning(
"[TNC] ARQ | RX | wrong byteorder check failed", e=e
)
# if frame buffer ends not with the current frame, we are going to append new data # if frame buffer ends not with the current frame, we are going to append new data
# if data already exists, we received the frame correctly, # if data already exists, we received the frame correctly,
# but the ACK frame didn't receive its destination (ISS) # but the ACK frame didn't receive its destination (ISS)
@ -657,6 +717,11 @@ class DATA:
# static.RX_FRAME_BUFFER --> existing data # static.RX_FRAME_BUFFER --> existing data
# temp_burst_buffer --> new data # temp_burst_buffer --> new data
# search_area --> area where we want to search # search_area --> area where we want to search
#data_mode = self.mode_list[self.speed_level]
#payload_per_frame = modem.get_bytes_per_frame(data_mode) - 2
#search_area = payload_per_frame - 3 # (3 bytes arq frame header)
search_area = 510 - 3 # (3 bytes arq frame header) search_area = 510 - 3 # (3 bytes arq frame header)
search_position = len(static.RX_FRAME_BUFFER) - search_area search_position = len(static.RX_FRAME_BUFFER) - search_area
@ -711,7 +776,7 @@ class DATA:
self.set_listening_modes(False, True, self.mode_list[self.speed_level]) self.set_listening_modes(False, True, self.mode_list[self.speed_level])
# Create and send ACK frame # Create and send ACK frame
self.log.info("[TNC] ARQ | RX | SENDING ACK") self.log.info("[TNC] ARQ | RX | SENDING ACK", finished=static.ARQ_SECONDS_UNTIL_FINISH, bytesperminute=static.ARQ_BYTES_PER_MINUTE)
self.send_burst_ack_frame(snr) self.send_burst_ack_frame(snr)
# Reset n retries per burst counter # Reset n retries per burst counter
@ -732,6 +797,7 @@ class DATA:
bytesperminute=static.ARQ_BYTES_PER_MINUTE, bytesperminute=static.ARQ_BYTES_PER_MINUTE,
mycallsign=str(self.mycallsign, 'UTF-8'), mycallsign=str(self.mycallsign, 'UTF-8'),
dxcallsign=str(self.dxcallsign, 'UTF-8'), dxcallsign=str(self.dxcallsign, 'UTF-8'),
finished=static.ARQ_SECONDS_UNTIL_FINISH,
) )
elif rx_n_frame_of_burst == rx_n_frames_per_burst - 1: elif rx_n_frame_of_burst == rx_n_frames_per_burst - 1:
@ -812,8 +878,10 @@ class DATA:
# transmittion duration # transmittion duration
duration = time.time() - self.rx_start_of_transmission duration = time.time() - self.rx_start_of_transmission
self.log.info("[TNC] ARQ | RX | DATA FRAME SUCCESSFULLY RECEIVED", nacks=self.frame_nack_counter,bytesperminute=static.ARQ_BYTES_PER_MINUTE, total_bytes=static.TOTAL_BYTES, duration=duration self.calculate_transfer_rate_rx(
) self.rx_start_of_transmission, len(static.RX_FRAME_BUFFER)
)
self.log.info("[TNC] ARQ | RX | DATA FRAME SUCCESSFULLY RECEIVED", nacks=self.frame_nack_counter,bytesperminute=static.ARQ_BYTES_PER_MINUTE, total_bytes=static.TOTAL_BYTES, duration=duration)
# Decompress the data frame # Decompress the data frame
data_frame_decompressed = lzma.decompress(data_frame) data_frame_decompressed = lzma.decompress(data_frame)
@ -945,11 +1013,12 @@ class DATA:
overflows=static.BUFFER_OVERFLOW_COUNTER, overflows=static.BUFFER_OVERFLOW_COUNTER,
nacks=self.frame_nack_counter, nacks=self.frame_nack_counter,
duration=duration, duration=duration,
bytesperminute=static.ARQ_BYTES_PER_MINUTE bytesperminute=static.ARQ_BYTES_PER_MINUTE,
data=data_frame,
) )
self.log.info("[TNC] ARQ | RX | Sending NACK") self.log.info("[TNC] ARQ | RX | Sending NACK", finished=static.ARQ_SECONDS_UNTIL_FINISH, bytesperminute=static.ARQ_BYTES_PER_MINUTE)
self.send_burst_nack_frame(snr) self.send_burst_nack_frame(snr)
# Update arq_session timestamp # Update arq_session timestamp
@ -990,6 +1059,7 @@ class DATA:
uuid=self.transmission_uuid, uuid=self.transmission_uuid,
percent=static.ARQ_TRANSMISSION_PERCENT, percent=static.ARQ_TRANSMISSION_PERCENT,
bytesperminute=static.ARQ_BYTES_PER_MINUTE, bytesperminute=static.ARQ_BYTES_PER_MINUTE,
finished=static.ARQ_SECONDS_UNTIL_FINISH,
mycallsign=str(self.mycallsign, 'UTF-8'), mycallsign=str(self.mycallsign, 'UTF-8'),
dxcallsign=str(self.dxcallsign, 'UTF-8'), dxcallsign=str(self.dxcallsign, 'UTF-8'),
) )
@ -1024,7 +1094,7 @@ class DATA:
+ data_out + data_out
+ self.data_frame_eof + self.data_frame_eof
) )
self.log.debug("[TNC] frame raw data:", data=data_out)
# Initial bufferposition is 0 # Initial bufferposition is 0
bufferposition = bufferposition_end = 0 bufferposition = bufferposition_end = 0
@ -1184,6 +1254,7 @@ class DATA:
uuid=self.transmission_uuid, uuid=self.transmission_uuid,
percent=static.ARQ_TRANSMISSION_PERCENT, percent=static.ARQ_TRANSMISSION_PERCENT,
bytesperminute=static.ARQ_BYTES_PER_MINUTE, bytesperminute=static.ARQ_BYTES_PER_MINUTE,
finished=static.ARQ_SECONDS_UNTIL_FINISH,
irs_snr=self.burst_ack_snr, irs_snr=self.burst_ack_snr,
mycallsign=str(self.mycallsign, 'UTF-8'), mycallsign=str(self.mycallsign, 'UTF-8'),
dxcallsign=str(self.dxcallsign, 'UTF-8'), dxcallsign=str(self.dxcallsign, 'UTF-8'),
@ -1209,6 +1280,7 @@ class DATA:
uuid=self.transmission_uuid, uuid=self.transmission_uuid,
percent=static.ARQ_TRANSMISSION_PERCENT, percent=static.ARQ_TRANSMISSION_PERCENT,
bytesperminute=static.ARQ_BYTES_PER_MINUTE, bytesperminute=static.ARQ_BYTES_PER_MINUTE,
finished=static.ARQ_SECONDS_UNTIL_FINISH,
mycallsign=str(self.mycallsign, 'UTF-8'), mycallsign=str(self.mycallsign, 'UTF-8'),
dxcallsign=str(self.dxcallsign, 'UTF-8'), dxcallsign=str(self.dxcallsign, 'UTF-8'),
) )
@ -1441,7 +1513,7 @@ class DATA:
) )
# wait while timeout not reached and our busy state is busy # wait while timeout not reached and our busy state is busy
channel_busy_timeout = time.time() + 30 channel_busy_timeout = time.time() + 15
while static.CHANNEL_BUSY and time.time() < channel_busy_timeout: while static.CHANNEL_BUSY and time.time() < channel_busy_timeout:
threading.Event().wait(0.01) threading.Event().wait(0.01)
@ -1594,6 +1666,14 @@ class DATA:
if not static.RESPOND_TO_CALL: if not static.RESPOND_TO_CALL:
return False return False
# ignore channel opener if already in ARQ STATE
# use case: Station A is connecting to Station B while
# Station B already tries connecting to Station A.
# For avoiding ignoring repeated connect request in case of packet loss
# we are only ignoring packets in case we are ISS
if static.ARQ_SESSION and self.IS_ARQ_SESSION_MASTER:
return False
self.IS_ARQ_SESSION_MASTER = False self.IS_ARQ_SESSION_MASTER = False
static.ARQ_SESSION_STATE = "connecting" static.ARQ_SESSION_STATE = "connecting"
@ -1927,25 +2007,10 @@ class DATA:
) )
# wait while timeout not reached and our busy state is busy # wait while timeout not reached and our busy state is busy
channel_busy_timeout = time.time() + 30 channel_busy_timeout = time.time() + 10
while static.CHANNEL_BUSY and time.time() < channel_busy_timeout: while static.CHANNEL_BUSY and time.time() < channel_busy_timeout:
threading.Event().wait(0.01) threading.Event().wait(0.01)
# if channel busy timeout reached, stop connecting
if time.time() > channel_busy_timeout:
self.log.warning("[TNC] Channel busy, try again later...")
static.ARQ_SESSION_STATE = "failed"
self.send_data_to_socket_queue(
freedata="tnc-message",
arq="transmission",
status="failed",
reason="busy",
mycallsign=str(self.mycallsign, 'UTF-8'),
dxcallsign=str(self.dxcallsign, 'UTF-8'),
)
static.ARQ_SESSION_STATE = "disconnected"
return False
self.enqueue_frame_for_tx([connection_frame], c2_mode=FREEDV_MODE.datac0.value, copies=1, repeat_delay=0) self.enqueue_frame_for_tx([connection_frame], c2_mode=FREEDV_MODE.datac0.value, copies=1, repeat_delay=0)
timeout = time.time() + 3 timeout = time.time() + 3
@ -2015,6 +2080,14 @@ class DATA:
# check if callsign ssid override # check if callsign ssid override
_, self.mycallsign = helpers.check_callsign(self.mycallsign, data_in[1:4]) _, self.mycallsign = helpers.check_callsign(self.mycallsign, data_in[1:4])
# ignore channel opener if already in ARQ STATE
# use case: Station A is connecting to Station B while
# Station B already tries connecting to Station A.
# For avoiding ignoring repeated connect request in case of packet loss
# we are only ignoring packets in case we are ISS
if static.ARQ_STATE and not self.is_IRS:
return False
static.DXCALLSIGN_CRC = bytes(data_in[4:7]) static.DXCALLSIGN_CRC = bytes(data_in[4:7])
self.dxcallsign = helpers.bytes_to_callsign(bytes(data_in[7:13])) self.dxcallsign = helpers.bytes_to_callsign(bytes(data_in[7:13]))
static.DXCALLSIGN = self.dxcallsign static.DXCALLSIGN = self.dxcallsign
@ -2100,7 +2173,6 @@ class DATA:
) )
self.session_id = data_in[13:14] self.session_id = data_in[13:14]
print(self.session_id)
# check again if callsign ssid override # check again if callsign ssid override
_, self.mycallsign = helpers.check_callsign(self.mycallsign, data_in[1:4]) _, self.mycallsign = helpers.check_callsign(self.mycallsign, data_in[1:4])
@ -2114,14 +2186,17 @@ class DATA:
channel_constellation=constellation, channel_constellation=constellation,
) )
# Reset data_channel/burst timestamps
self.data_channel_last_received = int(time.time())
self.burst_last_received = int(time.time())
# Set ARQ State AFTER resetting timeouts
# this avoids timeouts starting too early
static.ARQ_STATE = True static.ARQ_STATE = True
static.TNC_STATE = "BUSY" static.TNC_STATE = "BUSY"
self.reset_statistics() self.reset_statistics()
# Update data_channel timestamp
self.data_channel_last_received = int(time.time())
# Select the frame type based on the current TNC mode # Select the frame type based on the current TNC mode
if static.LOW_BANDWIDTH_MODE or self.received_LOW_BANDWIDTH_MODE: if static.LOW_BANDWIDTH_MODE or self.received_LOW_BANDWIDTH_MODE:
frametype = bytes([FR_TYPE.ARQ_DC_OPEN_ACK_N.value]) frametype = bytes([FR_TYPE.ARQ_DC_OPEN_ACK_N.value])
@ -2163,8 +2238,9 @@ class DATA:
# set start of transmission for our statistics # set start of transmission for our statistics
self.rx_start_of_transmission = time.time() self.rx_start_of_transmission = time.time()
# Update data_channel timestamp # Reset data_channel/burst timestamps once again for avoiding running into timeout
self.data_channel_last_received = int(time.time()) self.data_channel_last_received = int(time.time())
self.burst_last_received = int(time.time())
def arq_received_channel_is_open(self, data_in: bytes) -> None: def arq_received_channel_is_open(self, data_in: bytes) -> None:
""" """
@ -2240,7 +2316,6 @@ class DATA:
own=static.ARQ_PROTOCOL_VERSION, own=static.ARQ_PROTOCOL_VERSION,
) )
self.stop_transmission() self.stop_transmission()
self.arq_cleanup()
# ---------- PING # ---------- PING
def transmit_ping(self, mycallsign: bytes, dxcallsign: bytes) -> None: def transmit_ping(self, mycallsign: bytes, dxcallsign: bytes) -> None:
@ -2313,6 +2388,8 @@ class DATA:
snr=static.SNR, snr=static.SNR,
) )
static.DXGRID = b'------'
helpers.add_to_heard_stations( helpers.add_to_heard_stations(
dxcallsign, dxcallsign,
static.DXGRID, static.DXGRID,
@ -2409,7 +2486,6 @@ class DATA:
""" """
self.log.warning("[TNC] Stopping transmission!") self.log.warning("[TNC] Stopping transmission!")
static.TNC_STATE = "IDLE" static.TNC_STATE = "IDLE"
static.ARQ_STATE = False static.ARQ_STATE = False
self.send_data_to_socket_queue( self.send_data_to_socket_queue(
@ -2728,10 +2804,16 @@ class DATA:
static.ARQ_BYTES_PER_MINUTE = int( static.ARQ_BYTES_PER_MINUTE = int(
receivedbytes / (transmissiontime / 60) receivedbytes / (transmissiontime / 60)
) )
static.ARQ_SECONDS_UNTIL_FINISH = int(((static.TOTAL_BYTES - receivedbytes) / (static.ARQ_BYTES_PER_MINUTE * static.ARQ_COMPRESSION_FACTOR)) * 60) -20 # offset because of frame ack/nack
speed_chart = {"snr": static.SNR, "bpm": static.ARQ_BYTES_PER_MINUTE, "timestamp": int(time.time())}
# check if data already in list
if speed_chart not in static.SPEED_LIST:
static.SPEED_LIST.append(speed_chart)
else: else:
static.ARQ_BITS_PER_SECOND = 0 static.ARQ_BITS_PER_SECOND = 0
static.ARQ_BYTES_PER_MINUTE = 0 static.ARQ_BYTES_PER_MINUTE = 0
static.ARQ_SECONDS_UNTIL_FINISH = 0
except Exception as err: except Exception as err:
self.log.error(f"[TNC] calculate_transfer_rate_rx: Exception: {err}") self.log.error(f"[TNC] calculate_transfer_rate_rx: Exception: {err}")
static.ARQ_TRANSMISSION_PERCENT = 0.0 static.ARQ_TRANSMISSION_PERCENT = 0.0
@ -2755,6 +2837,7 @@ class DATA:
static.ARQ_BITS_PER_SECOND = 0 static.ARQ_BITS_PER_SECOND = 0
static.ARQ_TRANSMISSION_PERCENT = 0 static.ARQ_TRANSMISSION_PERCENT = 0
static.TOTAL_BYTES = 0 static.TOTAL_BYTES = 0
static.ARQ_SECONDS_UNTIL_FINISH = 0
def calculate_transfer_rate_tx( def calculate_transfer_rate_tx(
self, tx_start_of_transmission: float, sentbytes: int, tx_buffer_length: int self, tx_start_of_transmission: float, sentbytes: int, tx_buffer_length: int
@ -2781,10 +2864,18 @@ class DATA:
if sentbytes > 0: if sentbytes > 0:
static.ARQ_BITS_PER_SECOND = int((sentbytes * 8) / transmissiontime) static.ARQ_BITS_PER_SECOND = int((sentbytes * 8) / transmissiontime)
static.ARQ_BYTES_PER_MINUTE = int(sentbytes / (transmissiontime / 60)) static.ARQ_BYTES_PER_MINUTE = int(sentbytes / (transmissiontime / 60))
static.ARQ_SECONDS_UNTIL_FINISH = int(((tx_buffer_length - sentbytes) / (static.ARQ_BYTES_PER_MINUTE* static.ARQ_COMPRESSION_FACTOR)) * 60 )
speed_chart = {"snr": self.burst_ack_snr, "bpm": static.ARQ_BYTES_PER_MINUTE, "timestamp": int(time.time())}
# check if data already in list
if speed_chart not in static.SPEED_LIST:
static.SPEED_LIST.append(speed_chart)
else: else:
static.ARQ_BITS_PER_SECOND = 0 static.ARQ_BITS_PER_SECOND = 0
static.ARQ_BYTES_PER_MINUTE = 0 static.ARQ_BYTES_PER_MINUTE = 0
static.ARQ_SECONDS_UNTIL_FINISH = 0
except Exception as err: except Exception as err:
self.log.error(f"[TNC] calculate_transfer_rate_tx: Exception: {err}") self.log.error(f"[TNC] calculate_transfer_rate_tx: Exception: {err}")
@ -2809,7 +2900,7 @@ class DATA:
self.log.debug("[TNC] arq_cleanup") self.log.debug("[TNC] arq_cleanup")
self.session_id = bytes(1)
self.rx_frame_bof_received = False self.rx_frame_bof_received = False
self.rx_frame_eof_received = False self.rx_frame_eof_received = False
self.burst_ack = False self.burst_ack = False
@ -2817,7 +2908,7 @@ class DATA:
self.data_frame_ack_received = False self.data_frame_ack_received = False
static.RX_BURST_BUFFER = [] static.RX_BURST_BUFFER = []
static.RX_FRAME_BUFFER = b"" static.RX_FRAME_BUFFER = b""
self.burst_ack_snr = 255 self.burst_ack_snr = 0
# reset modem receiving state to reduce cpu load # reset modem receiving state to reduce cpu load
modem.RECEIVE_SIG0 = True modem.RECEIVE_SIG0 = True
@ -2848,11 +2939,14 @@ class DATA:
self.session_connect_max_retries = 10 self.session_connect_max_retries = 10
self.data_channel_max_retries = 10 self.data_channel_max_retries = 10
# we need to keep these values if in ARQ_SESSION
if not static.ARQ_SESSION: if not static.ARQ_SESSION:
static.TNC_STATE = "IDLE" static.TNC_STATE = "IDLE"
self.dxcallsign = b"AA0AA-0" self.dxcallsign = b"AA0AA-0"
self.mycallsign = static.MYCALLSIGN self.mycallsign = static.MYCALLSIGN
self.session_id = bytes(1)
static.SPEED_LIST = []
static.ARQ_STATE = False static.ARQ_STATE = False
self.arq_file_transfer = False self.arq_file_transfer = False
@ -2938,10 +3032,13 @@ class DATA:
modem_error_state = modem.get_modem_error_state() modem_error_state = modem.get_modem_error_state()
# We want to reach this state only if connected ( == return above not called ) # We want to reach this state only if connected ( == return above not called )
if ( timeout = self.burst_last_received + self.time_list[self.speed_level]
self.data_channel_last_received + self.time_list[self.speed_level] if timeout <= time.time() or modem_error_state:
<= time.time() or modem_error_state print("timeout----------------")
): print(time.time() - timeout)
print(time.time() - (self.burst_last_received + self.time_list[self.speed_level]))
print("-----------------------")
if modem_error_state: if modem_error_state:
self.log.warning( self.log.warning(
"[TNC] Decoding Error", "[TNC] Decoding Error",
@ -2951,11 +3048,15 @@ class DATA:
) )
else: else:
self.log.warning( self.log.warning(
"[TNC] Frame timeout", "[TNC] Burst timeout",
attempt=self.n_retries_per_burst, attempt=self.n_retries_per_burst,
max_attempts=self.rx_n_max_retries_per_burst, max_attempts=self.rx_n_max_retries_per_burst,
speed_level=self.speed_level, speed_level=self.speed_level,
) )
# reset self.burst_last_received
self.burst_last_received = time.time() + self.time_list[self.speed_level]
# reduce speed level if nack counter increased # reduce speed level if nack counter increased
self.frame_received_counter = 0 self.frame_received_counter = 0
self.burst_nack_counter += 1 self.burst_nack_counter += 1
@ -2980,7 +3081,6 @@ class DATA:
if self.n_retries_per_burst >= self.rx_n_max_retries_per_burst: if self.n_retries_per_burst >= self.rx_n_max_retries_per_burst:
self.stop_transmission() self.stop_transmission()
self.arq_cleanup()
def data_channel_keep_alive_watchdog(self) -> None: def data_channel_keep_alive_watchdog(self) -> None:
""" """
@ -2995,9 +3095,11 @@ class DATA:
> time.time() > time.time()
): ):
timeleft = (self.data_channel_last_received + self.transmission_timeout) - time.time() timeleft = int((self.data_channel_last_received + self.transmission_timeout) - time.time())
self.log.debug("Time left until timeout", seconds=timeleft) if timeleft % 10 == 0:
threading.Event().wait(5) self.log.debug("Time left until timeout", seconds=timeleft)
# threading.Event().wait(5)
# print(self.data_channel_last_received + self.transmission_timeout - time.time()) # print(self.data_channel_last_received + self.transmission_timeout - time.time())
# pass # pass
else: else:
@ -3071,8 +3173,10 @@ class DATA:
def send_test_frame(self) -> None: def send_test_frame(self) -> None:
"""Send an empty test frame""" """Send an empty test frame"""
test_frame = bytearray(126)
test_frame[:1] = bytes([FR_TYPE.TEST_FRAME.value])
self.enqueue_frame_for_tx( self.enqueue_frame_for_tx(
frame_to_tx=[bytearray(126)], c2_mode=FREEDV_MODE.datac3.value frame_to_tx=[test_frame], c2_mode=FREEDV_MODE.datac3.value
) )
def save_data_to_folder(self, def save_data_to_folder(self,

View file

@ -49,11 +49,12 @@ class explorer():
try: try:
callsign = str(i[0], "UTF-8") callsign = str(i[0], "UTF-8")
grid = str(i[1], "UTF-8") grid = str(i[1], "UTF-8")
timestamp = i[2]
try: try:
snr = i[4].split("/")[1] snr = i[4].split("/")[1]
except AttributeError: except AttributeError:
snr = str(i[4]) snr = str(i[4])
station_data["lastheard"].append({"callsign": callsign, "grid": grid, "snr": snr}) station_data["lastheard"].append({"callsign": callsign, "grid": grid, "snr": snr, "timestamp": timestamp})
except Exception as e: except Exception as e:
log.debug("[EXPLORER] not publishing station", e=e) log.debug("[EXPLORER] not publishing station", e=e)

View file

@ -5,7 +5,7 @@ Created on Fri Dec 25 21:25:14 2020
@author: DJ2LS @author: DJ2LS
""" """
import time import time
from datetime import datetime,timezone
import crcengine import crcengine
import static import static
import structlog import structlog
@ -132,7 +132,7 @@ def add_to_heard_stations(dxcallsign, dxgrid, datatype, snr, offset, frequency):
# check if buffer empty # check if buffer empty
if len(static.HEARD_STATIONS) == 0: if len(static.HEARD_STATIONS) == 0:
static.HEARD_STATIONS.append( static.HEARD_STATIONS.append(
[dxcallsign, dxgrid, int(time.time()), datatype, snr, offset, frequency] [dxcallsign, dxgrid, int(datetime.now(timezone.utc).timestamp()), datatype, snr, offset, frequency]
) )
# if not, we search and update # if not, we search and update
else: else:
@ -316,7 +316,7 @@ def check_callsign(callsign: bytes, crc_to_check: bytes):
log.debug("[HLP] check_callsign matched:", call_with_ssid=call_with_ssid) log.debug("[HLP] check_callsign matched:", call_with_ssid=call_with_ssid)
return [True, bytes(call_with_ssid)] return [True, bytes(call_with_ssid)]
return [False, ""] return [False, b'']
def check_session_id(id: bytes, id_to_check: bytes): def check_session_id(id: bytes, id_to_check: bytes):

View file

@ -14,7 +14,7 @@ def setup_logging(filename: str = "", level: str = "DEBUG"):
""" """
timestamper = structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S") timestamper = structlog.processors.TimeStamper(fmt="iso")
pre_chain = [ pre_chain = [
# Add the log level and a timestamp to the event_dict if the log entry # Add the log level and a timestamp to the event_dict if the log entry
# is not from structlog. # is not from structlog.

View file

@ -31,6 +31,7 @@ import modem
import static import static
import structlog import structlog
import explorer import explorer
import json
log = structlog.get_logger("main") log = structlog.get_logger("main")
@ -348,7 +349,9 @@ if __name__ == "__main__":
static.MYCALLSIGN = helpers.bytes_to_callsign(mycallsign) static.MYCALLSIGN = helpers.bytes_to_callsign(mycallsign)
static.MYCALLSIGN_CRC = helpers.get_crc_24(static.MYCALLSIGN) static.MYCALLSIGN_CRC = helpers.get_crc_24(static.MYCALLSIGN)
static.SSID_LIST = config['STATION']['ssid_list'] #json.loads = for converting str list to list
static.SSID_LIST = json.loads(config['STATION']['ssid_list'])
static.MYGRID = bytes(config['STATION']['mygrid'], "utf-8") static.MYGRID = bytes(config['STATION']['mygrid'], "utf-8")
# check if we have an int or str as device name # check if we have an int or str as device name
try: try:
@ -389,6 +392,11 @@ if __name__ == "__main__":
except Exception as e: except Exception as e:
log.warning("[CFG] Error", e=e) log.warning("[CFG] Error", e=e)
# make sure the own ssid is always part of the ssid list
my_ssid = int(static.MYCALLSIGN.split(b'-')[1])
if my_ssid not in static.SSID_LIST:
static.SSID_LIST.append(my_ssid)
# we need to wait until we got all parameters from argparse first before we can load the other modules # we need to wait until we got all parameters from argparse first before we can load the other modules
import sock import sock

View file

@ -25,7 +25,7 @@ import sounddevice as sd
import static import static
import structlog import structlog
import ujson as json import ujson as json
from queues import DATA_QUEUE_RECEIVED, MODEM_RECEIVED_QUEUE, MODEM_TRANSMIT_QUEUE from queues import DATA_QUEUE_RECEIVED, MODEM_RECEIVED_QUEUE, MODEM_TRANSMIT_QUEUE, RIGCTLD_COMMAND_QUEUE
TESTMODE = False TESTMODE = False
RXCHANNEL = "" RXCHANNEL = ""
@ -280,6 +280,13 @@ class RF:
) )
hamlib_thread.start() hamlib_thread.start()
hamlib_set_thread = threading.Thread(
target=self.set_rig_data, name="HAMLIB_SET_THREAD", daemon=True
)
hamlib_set_thread.start()
# self.log.debug("[MDM] Starting worker_receive") # self.log.debug("[MDM] Starting worker_receive")
worker_received = threading.Thread( worker_received = threading.Thread(
target=self.worker_received, name="WORKER_THREAD", daemon=True target=self.worker_received, name="WORKER_THREAD", daemon=True
@ -321,7 +328,7 @@ class RF:
# (self.fsk_ldpc_buffer_1, static.ENABLE_FSK), # (self.fsk_ldpc_buffer_1, static.ENABLE_FSK),
]: ]:
if ( if (
not data_buffer.nbuffer + length_x > data_buffer.size not (data_buffer.nbuffer + length_x) > data_buffer.size
and receive and receive
): ):
data_buffer.push(x) data_buffer.push(x)
@ -378,7 +385,7 @@ class RF:
(self.fsk_ldpc_buffer_0, static.ENABLE_FSK, 4), (self.fsk_ldpc_buffer_0, static.ENABLE_FSK, 4),
(self.fsk_ldpc_buffer_1, static.ENABLE_FSK, 5), (self.fsk_ldpc_buffer_1, static.ENABLE_FSK, 5),
]: ]:
if audiobuffer.nbuffer + length_x > audiobuffer.size: if (audiobuffer.nbuffer + length_x) > audiobuffer.size:
static.BUFFER_OVERFLOW_COUNTER[index] += 1 static.BUFFER_OVERFLOW_COUNTER[index] += 1
elif receive: elif receive:
audiobuffer.push(x) audiobuffer.push(x)
@ -596,6 +603,7 @@ class RF:
bytes_out, bytes_out,
bytes_per_frame, bytes_per_frame,
state_buffer, state_buffer,
mode_name,
) -> int: ) -> int:
""" """
De-modulate supplied audio stream with supplied codec2 instance. De-modulate supplied audio stream with supplied codec2 instance.
@ -613,6 +621,8 @@ class RF:
:type bytes_per_frame: int :type bytes_per_frame: int
:param state_buffer: modem states :param state_buffer: modem states
:type state_buffer: int :type state_buffer: int
:param mode_name: mode name
:type mode_name: str
:return: NIN from freedv instance :return: NIN from freedv instance
:rtype: int :rtype: int
""" """
@ -631,9 +641,20 @@ class RF:
# 3 trial sync # 3 trial sync
# 6 decoded # 6 decoded
# 10 error decoding == NACK # 10 error decoding == NACK
state = codec2.api.freedv_get_rx_status(freedv) rx_status = codec2.api.freedv_get_rx_status(freedv)
if state == 10:
state_buffer.append(state) if rx_status != 0:
# if we're receiving FreeDATA signals, reset channel busy state
static.CHANNEL_BUSY = False
self.log.debug(
"[MDM] [demod_audio] modem state", mode=mode_name, rx_status=rx_status, sync_flag=codec2.api.rx_sync_flags_to_text[rx_status]
)
if rx_status == 10:
state_buffer.append(rx_status)
audiobuffer.pop(nin) audiobuffer.pop(nin)
nin = codec2.api.freedv_nin(freedv) nin = codec2.api.freedv_nin(freedv)
@ -734,7 +755,8 @@ class RF:
self.sig0_datac0_freedv, self.sig0_datac0_freedv,
self.sig0_datac0_bytes_out, self.sig0_datac0_bytes_out,
self.sig0_datac0_bytes_per_frame, self.sig0_datac0_bytes_per_frame,
SIG0_DATAC0_STATE SIG0_DATAC0_STATE,
"sig0-datac0"
) )
def audio_sig1_datac0(self) -> None: def audio_sig1_datac0(self) -> None:
@ -745,7 +767,8 @@ class RF:
self.sig1_datac0_freedv, self.sig1_datac0_freedv,
self.sig1_datac0_bytes_out, self.sig1_datac0_bytes_out,
self.sig1_datac0_bytes_per_frame, self.sig1_datac0_bytes_per_frame,
SIG1_DATAC0_STATE SIG1_DATAC0_STATE,
"sig1-datac0"
) )
def audio_dat0_datac1(self) -> None: def audio_dat0_datac1(self) -> None:
@ -756,7 +779,8 @@ class RF:
self.dat0_datac1_freedv, self.dat0_datac1_freedv,
self.dat0_datac1_bytes_out, self.dat0_datac1_bytes_out,
self.dat0_datac1_bytes_per_frame, self.dat0_datac1_bytes_per_frame,
DAT0_DATAC1_STATE DAT0_DATAC1_STATE,
"dat0-datac1"
) )
def audio_dat0_datac3(self) -> None: def audio_dat0_datac3(self) -> None:
@ -767,7 +791,8 @@ class RF:
self.dat0_datac3_freedv, self.dat0_datac3_freedv,
self.dat0_datac3_bytes_out, self.dat0_datac3_bytes_out,
self.dat0_datac3_bytes_per_frame, self.dat0_datac3_bytes_per_frame,
DAT0_DATAC3_STATE DAT0_DATAC3_STATE,
"dat0-datac3"
) )
def audio_fsk_ldpc_0(self) -> None: def audio_fsk_ldpc_0(self) -> None:
@ -874,7 +899,6 @@ class RF:
# only take every tenth data point # only take every tenth data point
static.SCATTER = scatterdata[::10] static.SCATTER = scatterdata[::10]
def calculate_snr(self, freedv: ctypes.c_void_p) -> float: def calculate_snr(self, freedv: ctypes.c_void_p) -> float:
""" """
Ask codec2 for data about the received signal and calculate Ask codec2 for data about the received signal and calculate
@ -908,6 +932,20 @@ class RF:
static.SNR = 0 static.SNR = 0
return static.SNR return static.SNR
def set_rig_data(self) -> None:
"""
Set rigctld parameters like frequency, mode
THis needs to be processed in a queue
"""
while True:
cmd = RIGCTLD_COMMAND_QUEUE.get()
if cmd[0] == "set_frequency":
# [1] = Frequency
self.hamlib.set_frequency(cmd[1])
if cmd[0] == "set_mode":
# [1] = Mode
self.hamlib.set_mode(cmd[1])
def update_rig_data(self) -> None: def update_rig_data(self) -> None:
""" """
Request information about the current state of the radio via hamlib Request information about the current state of the radio via hamlib
@ -922,6 +960,7 @@ class RF:
static.HAMLIB_MODE = self.hamlib.get_mode() static.HAMLIB_MODE = self.hamlib.get_mode()
static.HAMLIB_BANDWIDTH = self.hamlib.get_bandwidth() static.HAMLIB_BANDWIDTH = self.hamlib.get_bandwidth()
static.HAMLIB_STATUS = self.hamlib.get_status() static.HAMLIB_STATUS = self.hamlib.get_status()
def calculate_fft(self) -> None: def calculate_fft(self) -> None:
""" """
Calculate an average signal strength of the channel to assess Calculate an average signal strength of the channel to assess
@ -974,6 +1013,8 @@ class RF:
# try except for avoiding runtime errors by division/0 # try except for avoiding runtime errors by division/0
try: try:
rms = int(np.sqrt(np.max(d ** 2))) rms = int(np.sqrt(np.max(d ** 2)))
if rms == 0:
raise ZeroDivisionError
static.AUDIO_DBFS = 20 * np.log10(rms / 32768) static.AUDIO_DBFS = 20 * np.log10(rms / 32768)
except Exception as e: except Exception as e:
self.log.warning( self.log.warning(
@ -1009,9 +1050,9 @@ class RF:
# so we have a smoother state toggle # so we have a smoother state toggle
if np.sum(dfft[dfft > avg + 15]) >= 400 and not static.TRANSMITTING: if np.sum(dfft[dfft > avg + 15]) >= 400 and not static.TRANSMITTING:
static.CHANNEL_BUSY = True static.CHANNEL_BUSY = True
# Limit delay counter to a maximum of 250. The higher this value, # Limit delay counter to a maximum of 200. The higher this value,
# the longer we will wait until releasing state # the longer we will wait until releasing state
channel_busy_delay = min(channel_busy_delay + 10, 250) channel_busy_delay = min(channel_busy_delay + 10, 200)
else: else:
# Decrement channel busy counter if no signal has been detected. # Decrement channel busy counter if no signal has been detected.
channel_busy_delay = max(channel_busy_delay - 1, 0) channel_busy_delay = max(channel_busy_delay - 1, 0)

View file

@ -13,3 +13,6 @@ MODEM_TRANSMIT_QUEUE = queue.Queue()
# Initialize FIFO queue to finally store received data # Initialize FIFO queue to finally store received data
RX_BUFFER = queue.Queue(maxsize=static.RX_BUFFER_SIZE) RX_BUFFER = queue.Queue(maxsize=static.RX_BUFFER_SIZE)
# Commands we want to send to rigctld
RIGCTLD_COMMAND_QUEUE = queue.Queue()

View file

@ -21,9 +21,11 @@ class radio:
def __init__(self, hostname="localhost", port=4532, poll_rate=5, timeout=5): def __init__(self, hostname="localhost", port=4532, poll_rate=5, timeout=5):
"""Open a connection to rigctld, and test it for validity""" """Open a connection to rigctld, and test it for validity"""
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.ptt_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.data_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.connected = False self.ptt_connected = False
self.data_connected = False
self.hostname = hostname self.hostname = hostname
self.port = port self.port = port
self.connection_attempts = 5 self.connection_attempts = 5
@ -33,7 +35,6 @@ class radio:
self.frequency = '' self.frequency = ''
self.mode = '' self.mode = ''
def open_rig( def open_rig(
self, self,
devicename, devicename,
@ -67,8 +68,20 @@ class radio:
self.hostname = rigctld_ip self.hostname = rigctld_ip
self.port = int(rigctld_port) self.port = int(rigctld_port)
if self.connect(): #_ptt_connect = self.ptt_connect()
self.log.debug("Rigctl initialized") #_data_connect = self.data_connect()
ptt_thread = threading.Thread(target=self.ptt_connect, args=[], daemon=True)
ptt_thread.start()
data_thread = threading.Thread(target=self.data_connect, args=[], daemon=True)
data_thread.start()
# wait some time
threading.Event().wait(0.5)
if self.ptt_connected and self.data_connected:
self.log.debug("Rigctl DATA/PTT initialized")
return True return True
self.log.error( self.log.error(
@ -76,33 +89,58 @@ class radio:
) )
return False return False
def connect(self): def ptt_connect(self):
"""Connect to rigctld instance""" """Connect to rigctld instance"""
if not self.connected: while True:
try:
self.connection = socket.create_connection((self.hostname, self.port)) if not self.ptt_connected:
self.connected = True try:
self.log.info( self.ptt_connection = socket.create_connection((self.hostname, self.port))
"[RIGCTLD] Connected to rigctld!", ip=self.hostname, port=self.port self.ptt_connected = True
) self.log.info(
return True "[RIGCTLD] Connected PTT instance to rigctld!", ip=self.hostname, port=self.port
except Exception as err: )
# ConnectionRefusedError: [Errno 111] Connection refused except Exception as err:
self.close_rig() # ConnectionRefusedError: [Errno 111] Connection refused
self.log.warning( self.close_rig()
"[RIGCTLD] Reconnect...", self.log.warning(
ip=self.hostname, "[RIGCTLD] PTT Reconnect...",
port=self.port, ip=self.hostname,
e=err, port=self.port,
) e=err,
return False )
threading.Event().wait(0.5)
def data_connect(self):
"""Connect to rigctld instance"""
while True:
if not self.data_connected:
try:
self.data_connection = socket.create_connection((self.hostname, self.port))
self.data_connected = True
self.log.info(
"[RIGCTLD] Connected DATA instance to rigctld!", ip=self.hostname, port=self.port
)
except Exception as err:
# ConnectionRefusedError: [Errno 111] Connection refused
self.close_rig()
self.log.warning(
"[RIGCTLD] DATA Reconnect...",
ip=self.hostname,
port=self.port,
e=err,
)
threading.Event().wait(0.5)
def close_rig(self): def close_rig(self):
""" """ """ """
self.sock.close() self.ptt_sock.close()
self.connected = False self.data_sock.close()
self.ptt_connected = False
self.data_connected = False
def send_command(self, command, expect_answer) -> bytes: def send_ptt_command(self, command, expect_answer) -> bytes:
"""Send a command to the connected rotctld instance, """Send a command to the connected rotctld instance,
and return the return value. and return the return value.
@ -110,9 +148,9 @@ class radio:
command: command:
""" """
if self.connected: if self.ptt_connected:
try: try:
self.connection.sendall(command + b"\n") self.ptt_connection.sendall(command + b"\n")
except Exception: except Exception:
self.log.warning( self.log.warning(
"[RIGCTLD] Command not executed!", "[RIGCTLD] Command not executed!",
@ -120,12 +158,33 @@ class radio:
ip=self.hostname, ip=self.hostname,
port=self.port, port=self.port,
) )
self.connected = False self.ptt_connected = False
return b""
def send_data_command(self, command, expect_answer) -> bytes:
"""Send a command to the connected rotctld instance,
and return the return value.
Args:
command:
"""
if self.data_connected:
try:
self.data_connection.sendall(command + b"\n")
except Exception:
self.log.warning(
"[RIGCTLD] Command not executed!",
command=command,
ip=self.hostname,
port=self.port,
)
self.data_connected = False
try: try:
# recv seems to be blocking so in case of ptt we dont need the response # recv seems to be blocking so in case of ptt we don't need the response
# maybe this speeds things up and avoids blocking states # maybe this speeds things up and avoids blocking states
return self.connection.recv(16) if expect_answer else True return self.data_connection.recv(64) if expect_answer else True
except Exception: except Exception:
self.log.warning( self.log.warning(
"[RIGCTLD] No command response!", "[RIGCTLD] No command response!",
@ -133,23 +192,17 @@ class radio:
ip=self.hostname, ip=self.hostname,
port=self.port, port=self.port,
) )
self.connected = False self.data_connected = False
else:
# reconnecting....
threading.Event().wait(0.5)
self.connect()
return b"" return b""
def get_status(self): def get_status(self):
""" """ """ """
return "connected" if self.connected else "unknown/disconnected" return "connected" if self.data_connected and self.ptt_connected else "unknown/disconnected"
def get_mode(self): def get_mode(self):
""" """ """ """
try: try:
data = self.send_command(b"m", True) data = self.send_data_command(b"m", True)
data = data.split(b"\n") data = data.split(b"\n")
data = data[0].decode("utf-8") data = data[0].decode("utf-8")
if 'RPRT' not in data: if 'RPRT' not in data:
@ -165,7 +218,7 @@ class radio:
def get_bandwidth(self): def get_bandwidth(self):
""" """ """ """
try: try:
data = self.send_command(b"m", True) data = self.send_data_command(b"m", True)
data = data.split(b"\n") data = data.split(b"\n")
data = data[1].decode("utf-8") data = data[1].decode("utf-8")
@ -179,7 +232,7 @@ class radio:
def get_frequency(self): def get_frequency(self):
""" """ """ """
try: try:
data = self.send_command(b"f", True) data = self.send_data_command(b"f", True)
data = data.decode("utf-8") data = data.decode("utf-8")
if 'RPRT' not in data and data not in [0, '0', '']: if 'RPRT' not in data and data not in [0, '0', '']:
with contextlib.suppress(ValueError): with contextlib.suppress(ValueError):
@ -209,9 +262,39 @@ class radio:
""" """
try: try:
if state: if state:
self.send_command(b"T 1", False) self.send_ptt_command(b"T 1", False)
else: else:
self.send_command(b"T 0", False) self.send_ptt_command(b"T 0", False)
return state return state
except Exception: except Exception:
return False return False
def set_frequency(self, frequency):
"""
Args:
frequency:
Returns:
"""
try:
command = bytes(f"F {frequency}", "utf-8")
self.send_data_command(command, False)
except Exception:
return False
def set_mode(self, mode):
"""
Args:
mode:
Returns:
"""
try:
command = bytes(f"M {mode} {self.bandwidth}", "utf-8")
self.send_data_command(command, False)
except Exception:
return False

View file

@ -30,6 +30,9 @@ class radio:
""" """ """ """
return None return None
def set_bandwidth(self):
""" """
return None
def set_mode(self, mode): def set_mode(self, mode):
""" """
@ -41,6 +44,16 @@ class radio:
""" """
return None return None
def set_frequency(self, frequency):
"""
Args:
mode:
Returns:
"""
return None
def get_status(self): def get_status(self):
""" """

View file

@ -31,7 +31,7 @@ import static
import structlog import structlog
import ujson as json import ujson as json
from exceptions import NoCallsign from exceptions import NoCallsign
from queues import DATA_QUEUE_TRANSMIT, RX_BUFFER from queues import DATA_QUEUE_TRANSMIT, RX_BUFFER, RIGCTLD_COMMAND_QUEUE
SOCKET_QUEUE = queue.Queue() SOCKET_QUEUE = queue.Queue()
DAEMON_QUEUE = queue.Queue() DAEMON_QUEUE = queue.Queue()
@ -136,7 +136,7 @@ class ThreadedTCPRequestHandler(socketserver.StreamRequestHandler):
# we might improve this by only processing one command or # we might improve this by only processing one command or
# doing some kind of selection to determin which commands need to be dropped # doing some kind of selection to determin which commands need to be dropped
# and which one can be processed during a running transmission # and which one can be processed during a running transmission
threading.Event().wait(3) threading.Event().wait(0.5)
# finally delete our rx buffer to be ready for new commands # finally delete our rx buffer to be ready for new commands
data = bytes() data = bytes()
@ -594,6 +594,32 @@ def process_tnc_commands(data):
command=received_json, command=received_json,
) )
# SET FREQUENCY -----------------------------------------------------
if received_json["command"] == "frequency" and received_json["type"] == "set":
try:
RIGCTLD_COMMAND_QUEUE.put(["set_frequency", received_json["frequency"]])
command_response("set_frequency", True)
except Exception as err:
command_response("set_frequency", False)
log.warning(
"[SCK] Set frequency command execution error",
e=err,
command=received_json,
)
# SET MODE -----------------------------------------------------
if received_json["command"] == "mode" and received_json["type"] == "set":
try:
RIGCTLD_COMMAND_QUEUE.put(["set_mode", received_json["mode"]])
command_response("set_mode", True)
except Exception as err:
command_response("set_mode", False)
log.warning(
"[SCK] Set mode command execution error",
e=err,
command=received_json,
)
# exception, if JSON cant be decoded # exception, if JSON cant be decoded
except Exception as err: except Exception as err:
log.error("[SCK] JSON decoding error", e=err) log.error("[SCK] JSON decoding error", e=err)
@ -625,12 +651,15 @@ def send_tnc_state():
"rx_msg_buffer_length": str(len(static.RX_MSG_BUFFER)), "rx_msg_buffer_length": str(len(static.RX_MSG_BUFFER)),
"arq_bytes_per_minute": str(static.ARQ_BYTES_PER_MINUTE), "arq_bytes_per_minute": str(static.ARQ_BYTES_PER_MINUTE),
"arq_bytes_per_minute_burst": str(static.ARQ_BYTES_PER_MINUTE_BURST), "arq_bytes_per_minute_burst": str(static.ARQ_BYTES_PER_MINUTE_BURST),
"arq_seconds_until_finish": str(static.ARQ_SECONDS_UNTIL_FINISH),
"arq_compression_factor": str(static.ARQ_COMPRESSION_FACTOR), "arq_compression_factor": str(static.ARQ_COMPRESSION_FACTOR),
"arq_transmission_percent": str(static.ARQ_TRANSMISSION_PERCENT), "arq_transmission_percent": str(static.ARQ_TRANSMISSION_PERCENT),
"speed_list": static.SPEED_LIST,
"total_bytes": str(static.TOTAL_BYTES), "total_bytes": str(static.TOTAL_BYTES),
"beacon_state": str(static.BEACON_STATE), "beacon_state": str(static.BEACON_STATE),
"stations": [], "stations": [],
"mycallsign": str(static.MYCALLSIGN, encoding), "mycallsign": str(static.MYCALLSIGN, encoding),
"mygrid": str(static.MYGRID, encoding),
"dxcallsign": str(static.DXCALLSIGN, encoding), "dxcallsign": str(static.DXCALLSIGN, encoding),
"dxgrid": str(static.DXGRID, encoding), "dxgrid": str(static.DXGRID, encoding),
"hamlib_status": static.HAMLIB_STATUS, "hamlib_status": static.HAMLIB_STATUS,
@ -651,7 +680,6 @@ def send_tnc_state():
"frequency": heard[6], "frequency": heard[6],
} }
) )
return json.dumps(output) return json.dumps(output)

View file

@ -11,7 +11,7 @@ Not nice, suggestions are appreciated :-)
import subprocess import subprocess
from enum import Enum from enum import Enum
VERSION = "0.6.9-alpha.1" VERSION = "0.6.11-alpha.1-exp"
ENABLE_EXPLORER = False ENABLE_EXPLORER = False
@ -95,12 +95,14 @@ CHANNEL_BUSY: bool = False
ARQ_PROTOCOL_VERSION: int = 5 ARQ_PROTOCOL_VERSION: int = 5
# ARQ statistics # ARQ statistics
SPEED_LIST: list = []
ARQ_BYTES_PER_MINUTE_BURST: int = 0 ARQ_BYTES_PER_MINUTE_BURST: int = 0
ARQ_BYTES_PER_MINUTE: int = 0 ARQ_BYTES_PER_MINUTE: int = 0
ARQ_BITS_PER_SECOND_BURST: int = 0 ARQ_BITS_PER_SECOND_BURST: int = 0
ARQ_BITS_PER_SECOND: int = 0 ARQ_BITS_PER_SECOND: int = 0
ARQ_COMPRESSION_FACTOR: int = 0 ARQ_COMPRESSION_FACTOR: int = 0
ARQ_TRANSMISSION_PERCENT: int = 0 ARQ_TRANSMISSION_PERCENT: int = 0
ARQ_SECONDS_UNTIL_FINISH: int = 0
ARQ_SPEED_LEVEL: int = 0 ARQ_SPEED_LEVEL: int = 0
TOTAL_BYTES: int = 0 TOTAL_BYTES: int = 0
# set save to folder state for allowing downloading files to local file system # set save to folder state for allowing downloading files to local file system