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-icons": "^1.9.1",
"bootswatch": "^5.2.0",
"chart.js": "^3.9.1",
"chartjs-plugin-annotation": "^2.0.1",
"chart.js": "^4.0.0",
"chartjs-plugin-annotation": "^2.1.2",
"electron-log": "^4.4.8",
"electron-updater": "^5.2.1",
"emoji-picker-element": "^1.12.1",

View file

@ -1,5 +1,5 @@
const path = require('path');
const {ipcRenderer} = require('electron');
const {ipcRenderer, shell} = require('electron');
const exec = require('child_process').spawn;
const sock = require('./sock.js');
const daemon = require('./daemon.js');
@ -37,8 +37,76 @@ var dbfs_level_raw = 0
// WINDOW LISTENER
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
document.getElementById("startStopRecording").addEventListener("click", () => {
@ -221,16 +289,57 @@ document.getElementById('openReceivedFilesFolder').addEventListener('click', ()
if (config.spectrum == 'waterfall') {
document.getElementById("waterfall-scatter-switch1").checked = true;
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.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-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.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
@ -703,21 +812,55 @@ document.getElementById('hamlib_rigctld_stop').addEventListener('click', () => {
// on click waterfall scatter toggle view
// waterfall
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("waterfall").style.display = 'block';
document.getElementById("waterfall").style.visibility = 'visible';
document.getElementById("waterfall").style.height = '100%';
config.spectrum = 'waterfall';
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
});
// scatter
document.getElementById("waterfall-scatter-switch2").addEventListener("click", () => {
document.getElementById("scatter").style.display = 'block';
document.getElementById("scatter").style.visibility = 'visible';
document.getElementById("scatter").style.height = '100%';
document.getElementById("waterfall").style.visibility = 'hidden';
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';
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
@ -1008,6 +1151,11 @@ document.getElementById('hamlib_rigctld_stop').addEventListener('click', () => {
sock.stopBeacon();
});
// Explorer button clicked
document.getElementById("openExplorer").addEventListener("click", () => {
shell.openExternal('https://explorer.freedata.app/?myCall=' + document.getElementById("myCall").value);
});
// startTNC button clicked
document.getElementById("startTNC").addEventListener("click", () => {
@ -1332,28 +1480,18 @@ ipcRenderer.on('action-update-tnc-state', (event, arg) => {
document.title = documentTitle[0] + 'Call: ' + arg.mycallsign;
}
// TOE TIME OF EXECUTION --> How many time needs a command to be executed until data arrives
// deactivated this feature, beacuse its useless at this time. maybe it is getting more interesting, if we are working via network
// 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
// update mygrid information with data from tnc
if (typeof(arg.mygrid) !== 'undefined') {
document.getElementById("myGrid").value = arg.mygrid;
}
document.getElementById("toe").innerHTML = toe + ' ms'
*/
// DATA STATE
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: {
legend: {
display: false,
@ -1407,16 +1545,12 @@ ipcRenderer.on('action-update-tnc-state', (event, arg) => {
}
}
}
var data = arg.scatter
var newdata = {
var scatterData = arg.scatter
var newScatterData = {
datasets: [{
//label: 'constellation diagram',
data: data,
options: config,
data: scatterData,
options: scatterConfig,
backgroundColor: 'rgb(255, 99, 132)'
}],
};
@ -1426,25 +1560,122 @@ ipcRenderer.on('action-update-tnc-state', (event, arg) => {
} else {
var scatterSize = arg.scatter.length;
}
if (global.data != newdata && scatterSize > 0) {
try {
global.myChart.destroy();
} catch (e) {
// myChart not yet created
console.log(e);
if (global.scatterData != newScatterData && scatterSize > 0) {
global.scatterData = newScatterData;
if (typeof(global.scatterChart) == 'undefined') {
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;
var ctx = document.getElementById('scatter').getContext('2d');
global.myChart = new Chart(ctx, {
type: 'scatter',
data: global.data,
options: config
if (typeof(global.speedChart) == 'undefined') {
var speedCtx = document.getElementById('chart').getContext('2d');
global.speedChart = new Chart(speedCtx, {
data: newSpeedData,
options: speedChartOptions
});
} else {
if(speedDataSnr.length > 0){
global.speedChart.data = newSpeedData;
global.speedChart.update();
}
}
// END OF SPEED CHART
// PTT STATE
if (arg.ptt_state == 'True') {
document.getElementById("ptt_state").className = "btn btn-sm btn-danger";
@ -1566,7 +1797,10 @@ ipcRenderer.on('action-update-tnc-state', (event, arg) => {
}
// 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
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);
}
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
@ -1608,6 +1865,7 @@ ipcRenderer.on('action-update-tnc-state', (event, arg) => {
if(arg.speed_level >= 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') {
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 = {
mycallsign: data['mycallsign'],
mygrid: data['mygrid'],
ptt_state: data['ptt_state'],
busy_state: data['tnc_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_n_arq_frames_per_data_frame: data['arq_n_arq_frames_per_data_frame'],
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'],
total_bytes: data['total_bytes'],
arq_transmission_percent: data['arq_transmission_percent'],
@ -229,6 +231,8 @@ client.on('data', function(socketdata) {
hamlib_status: data['hamlib_status'],
listen: data['listen'],
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);
@ -597,6 +601,19 @@ exports.record_audio = function() {
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) => {
client.destroy();
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>
</div>
<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!">
<!--
@ -837,37 +838,19 @@
<div class="card-header p-1">
<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>
<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">
<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>
<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 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-->
<canvas id="waterfall" style="position: relative; z-index: 2;"></canvas>
<canvas id="scatter" style="position: relative; z-index: 1;"></canvas>
<canvas id="waterfall" style="position: relative; z-index: 2; transform: translateZ(0);"></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>
@ -1066,28 +1049,64 @@
<nav class="navbar fixed-bottom navbar-light bg-light">
<div class="container-fluid">
<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>
</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>
</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>
</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>
</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>
</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">
<!--<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>
<!--<span class="input-group-text" id="basic-addon1"><strong>BW</strong></span>--><span class="input-group-text" id="bandwidth">---</span> </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="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 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">
@ -1100,6 +1119,7 @@
<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>
<!--<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>
@ -1107,8 +1127,8 @@
<!-- bootstrap -->
<script src="../node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<!-- chart.js -->
<script src="../node_modules/chart.js/dist/chart.min.js"></script>
<script src="../node_modules/chartjs-plugin-annotation/dist/chartjs-plugin-annotation.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="../ui.js"></script>-->
<!-- WATERFALL -->
<script src="waterfall/colormap.js"></script>
@ -1295,4 +1315,4 @@
</div>
</body>
</html>
</html>

View file

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

View file

@ -64,6 +64,7 @@ class DATA:
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.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
: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
# 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[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
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:
"""Build and send ACK frame for received DATA frame"""
@ -485,11 +495,19 @@ class DATA:
# ack_frame[7:8] = bytes([int(snr)])
# 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
# 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], 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:
# 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.
@ -532,7 +550,15 @@ class DATA:
# TRANSMIT NACK FRAME FOR BURST
# 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)
# 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)
# 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:
"""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[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
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:
"""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 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
# 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)
def arq_data_received(
@ -602,6 +641,7 @@ class DATA:
# Update data_channel timestamp
self.data_channel_last_received = int(time.time())
self.burst_last_received = int(time.time())
# Extract some important data from the frame
# Get sequence number of burst frame
@ -641,6 +681,26 @@ class DATA:
# static.RX_FRAME_BUFFER += static.RX_BURST_BUFFER[i]
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 data already exists, we received the frame correctly,
# but the ACK frame didn't receive its destination (ISS)
@ -657,6 +717,11 @@ class DATA:
# static.RX_FRAME_BUFFER --> existing data
# temp_burst_buffer --> new data
# 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_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])
# 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)
# Reset n retries per burst counter
@ -732,6 +797,7 @@ class DATA:
bytesperminute=static.ARQ_BYTES_PER_MINUTE,
mycallsign=str(self.mycallsign, '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:
@ -812,8 +878,10 @@ class DATA:
# transmittion duration
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
data_frame_decompressed = lzma.decompress(data_frame)
@ -945,11 +1013,12 @@ class DATA:
overflows=static.BUFFER_OVERFLOW_COUNTER,
nacks=self.frame_nack_counter,
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)
# Update arq_session timestamp
@ -990,6 +1059,7 @@ class DATA:
uuid=self.transmission_uuid,
percent=static.ARQ_TRANSMISSION_PERCENT,
bytesperminute=static.ARQ_BYTES_PER_MINUTE,
finished=static.ARQ_SECONDS_UNTIL_FINISH,
mycallsign=str(self.mycallsign, 'UTF-8'),
dxcallsign=str(self.dxcallsign, 'UTF-8'),
)
@ -1024,7 +1094,7 @@ class DATA:
+ data_out
+ self.data_frame_eof
)
self.log.debug("[TNC] frame raw data:", data=data_out)
# Initial bufferposition is 0
bufferposition = bufferposition_end = 0
@ -1184,6 +1254,7 @@ class DATA:
uuid=self.transmission_uuid,
percent=static.ARQ_TRANSMISSION_PERCENT,
bytesperminute=static.ARQ_BYTES_PER_MINUTE,
finished=static.ARQ_SECONDS_UNTIL_FINISH,
irs_snr=self.burst_ack_snr,
mycallsign=str(self.mycallsign, 'UTF-8'),
dxcallsign=str(self.dxcallsign, 'UTF-8'),
@ -1209,6 +1280,7 @@ class DATA:
uuid=self.transmission_uuid,
percent=static.ARQ_TRANSMISSION_PERCENT,
bytesperminute=static.ARQ_BYTES_PER_MINUTE,
finished=static.ARQ_SECONDS_UNTIL_FINISH,
mycallsign=str(self.mycallsign, '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
channel_busy_timeout = time.time() + 30
channel_busy_timeout = time.time() + 15
while static.CHANNEL_BUSY and time.time() < channel_busy_timeout:
threading.Event().wait(0.01)
@ -1594,6 +1666,14 @@ class DATA:
if not static.RESPOND_TO_CALL:
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
static.ARQ_SESSION_STATE = "connecting"
@ -1927,25 +2007,10 @@ class DATA:
)
# 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:
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)
timeout = time.time() + 3
@ -2015,6 +2080,14 @@ class DATA:
# check if callsign ssid override
_, 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])
self.dxcallsign = helpers.bytes_to_callsign(bytes(data_in[7:13]))
static.DXCALLSIGN = self.dxcallsign
@ -2100,7 +2173,6 @@ class DATA:
)
self.session_id = data_in[13:14]
print(self.session_id)
# check again if callsign ssid override
_, self.mycallsign = helpers.check_callsign(self.mycallsign, data_in[1:4])
@ -2114,14 +2186,17 @@ class DATA:
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.TNC_STATE = "BUSY"
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
if static.LOW_BANDWIDTH_MODE or self.received_LOW_BANDWIDTH_MODE:
frametype = bytes([FR_TYPE.ARQ_DC_OPEN_ACK_N.value])
@ -2163,8 +2238,9 @@ class DATA:
# set start of transmission for our statistics
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.burst_last_received = int(time.time())
def arq_received_channel_is_open(self, data_in: bytes) -> None:
"""
@ -2240,7 +2316,6 @@ class DATA:
own=static.ARQ_PROTOCOL_VERSION,
)
self.stop_transmission()
self.arq_cleanup()
# ---------- PING
def transmit_ping(self, mycallsign: bytes, dxcallsign: bytes) -> None:
@ -2313,6 +2388,8 @@ class DATA:
snr=static.SNR,
)
static.DXGRID = b'------'
helpers.add_to_heard_stations(
dxcallsign,
static.DXGRID,
@ -2409,7 +2486,6 @@ class DATA:
"""
self.log.warning("[TNC] Stopping transmission!")
static.TNC_STATE = "IDLE"
static.ARQ_STATE = False
self.send_data_to_socket_queue(
@ -2728,10 +2804,16 @@ class DATA:
static.ARQ_BYTES_PER_MINUTE = int(
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:
static.ARQ_BITS_PER_SECOND = 0
static.ARQ_BYTES_PER_MINUTE = 0
static.ARQ_SECONDS_UNTIL_FINISH = 0
except Exception as err:
self.log.error(f"[TNC] calculate_transfer_rate_rx: Exception: {err}")
static.ARQ_TRANSMISSION_PERCENT = 0.0
@ -2755,6 +2837,7 @@ class DATA:
static.ARQ_BITS_PER_SECOND = 0
static.ARQ_TRANSMISSION_PERCENT = 0
static.TOTAL_BYTES = 0
static.ARQ_SECONDS_UNTIL_FINISH = 0
def calculate_transfer_rate_tx(
self, tx_start_of_transmission: float, sentbytes: int, tx_buffer_length: int
@ -2781,10 +2864,18 @@ class DATA:
if sentbytes > 0:
static.ARQ_BITS_PER_SECOND = int((sentbytes * 8) / transmissiontime)
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:
static.ARQ_BITS_PER_SECOND = 0
static.ARQ_BYTES_PER_MINUTE = 0
static.ARQ_SECONDS_UNTIL_FINISH = 0
except Exception as 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.session_id = bytes(1)
self.rx_frame_bof_received = False
self.rx_frame_eof_received = False
self.burst_ack = False
@ -2817,7 +2908,7 @@ class DATA:
self.data_frame_ack_received = False
static.RX_BURST_BUFFER = []
static.RX_FRAME_BUFFER = b""
self.burst_ack_snr = 255
self.burst_ack_snr = 0
# reset modem receiving state to reduce cpu load
modem.RECEIVE_SIG0 = True
@ -2848,11 +2939,14 @@ class DATA:
self.session_connect_max_retries = 10
self.data_channel_max_retries = 10
# we need to keep these values if in ARQ_SESSION
if not static.ARQ_SESSION:
static.TNC_STATE = "IDLE"
self.dxcallsign = b"AA0AA-0"
self.mycallsign = static.MYCALLSIGN
self.session_id = bytes(1)
static.SPEED_LIST = []
static.ARQ_STATE = False
self.arq_file_transfer = False
@ -2938,10 +3032,13 @@ class DATA:
modem_error_state = modem.get_modem_error_state()
# We want to reach this state only if connected ( == return above not called )
if (
self.data_channel_last_received + self.time_list[self.speed_level]
<= time.time() or modem_error_state
):
timeout = self.burst_last_received + self.time_list[self.speed_level]
if timeout <= 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:
self.log.warning(
"[TNC] Decoding Error",
@ -2951,11 +3048,15 @@ class DATA:
)
else:
self.log.warning(
"[TNC] Frame timeout",
"[TNC] Burst timeout",
attempt=self.n_retries_per_burst,
max_attempts=self.rx_n_max_retries_per_burst,
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
self.frame_received_counter = 0
self.burst_nack_counter += 1
@ -2980,7 +3081,6 @@ class DATA:
if self.n_retries_per_burst >= self.rx_n_max_retries_per_burst:
self.stop_transmission()
self.arq_cleanup()
def data_channel_keep_alive_watchdog(self) -> None:
"""
@ -2995,9 +3095,11 @@ class DATA:
> time.time()
):
timeleft = (self.data_channel_last_received + self.transmission_timeout) - time.time()
self.log.debug("Time left until timeout", seconds=timeleft)
threading.Event().wait(5)
timeleft = int((self.data_channel_last_received + self.transmission_timeout) - time.time())
if timeleft % 10 == 0:
self.log.debug("Time left until timeout", seconds=timeleft)
# threading.Event().wait(5)
# print(self.data_channel_last_received + self.transmission_timeout - time.time())
# pass
else:
@ -3071,8 +3173,10 @@ class DATA:
def send_test_frame(self) -> None:
"""Send an empty test frame"""
test_frame = bytearray(126)
test_frame[:1] = bytes([FR_TYPE.TEST_FRAME.value])
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,

View file

@ -49,11 +49,12 @@ class explorer():
try:
callsign = str(i[0], "UTF-8")
grid = str(i[1], "UTF-8")
timestamp = i[2]
try:
snr = i[4].split("/")[1]
except AttributeError:
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:
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
"""
import time
from datetime import datetime,timezone
import crcengine
import static
import structlog
@ -132,7 +132,7 @@ def add_to_heard_stations(dxcallsign, dxgrid, datatype, snr, offset, frequency):
# check if buffer empty
if len(static.HEARD_STATIONS) == 0:
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
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)
return [True, bytes(call_with_ssid)]
return [False, ""]
return [False, b'']
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 = [
# Add the log level and a timestamp to the event_dict if the log entry
# is not from structlog.

View file

@ -31,6 +31,7 @@ import modem
import static
import structlog
import explorer
import json
log = structlog.get_logger("main")
@ -348,7 +349,9 @@ if __name__ == "__main__":
static.MYCALLSIGN = helpers.bytes_to_callsign(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")
# check if we have an int or str as device name
try:
@ -389,6 +392,11 @@ if __name__ == "__main__":
except Exception as 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
import sock

View file

@ -25,7 +25,7 @@ import sounddevice as sd
import static
import structlog
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
RXCHANNEL = ""
@ -280,6 +280,13 @@ class RF:
)
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")
worker_received = threading.Thread(
target=self.worker_received, name="WORKER_THREAD", daemon=True
@ -321,7 +328,7 @@ class RF:
# (self.fsk_ldpc_buffer_1, static.ENABLE_FSK),
]:
if (
not data_buffer.nbuffer + length_x > data_buffer.size
not (data_buffer.nbuffer + length_x) > data_buffer.size
and receive
):
data_buffer.push(x)
@ -378,7 +385,7 @@ class RF:
(self.fsk_ldpc_buffer_0, static.ENABLE_FSK, 4),
(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
elif receive:
audiobuffer.push(x)
@ -596,6 +603,7 @@ class RF:
bytes_out,
bytes_per_frame,
state_buffer,
mode_name,
) -> int:
"""
De-modulate supplied audio stream with supplied codec2 instance.
@ -613,6 +621,8 @@ class RF:
:type bytes_per_frame: int
:param state_buffer: modem states
:type state_buffer: int
:param mode_name: mode name
:type mode_name: str
:return: NIN from freedv instance
:rtype: int
"""
@ -631,9 +641,20 @@ class RF:
# 3 trial sync
# 6 decoded
# 10 error decoding == NACK
state = codec2.api.freedv_get_rx_status(freedv)
if state == 10:
state_buffer.append(state)
rx_status = codec2.api.freedv_get_rx_status(freedv)
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)
nin = codec2.api.freedv_nin(freedv)
@ -734,7 +755,8 @@ class RF:
self.sig0_datac0_freedv,
self.sig0_datac0_bytes_out,
self.sig0_datac0_bytes_per_frame,
SIG0_DATAC0_STATE
SIG0_DATAC0_STATE,
"sig0-datac0"
)
def audio_sig1_datac0(self) -> None:
@ -745,7 +767,8 @@ class RF:
self.sig1_datac0_freedv,
self.sig1_datac0_bytes_out,
self.sig1_datac0_bytes_per_frame,
SIG1_DATAC0_STATE
SIG1_DATAC0_STATE,
"sig1-datac0"
)
def audio_dat0_datac1(self) -> None:
@ -756,7 +779,8 @@ class RF:
self.dat0_datac1_freedv,
self.dat0_datac1_bytes_out,
self.dat0_datac1_bytes_per_frame,
DAT0_DATAC1_STATE
DAT0_DATAC1_STATE,
"dat0-datac1"
)
def audio_dat0_datac3(self) -> None:
@ -767,7 +791,8 @@ class RF:
self.dat0_datac3_freedv,
self.dat0_datac3_bytes_out,
self.dat0_datac3_bytes_per_frame,
DAT0_DATAC3_STATE
DAT0_DATAC3_STATE,
"dat0-datac3"
)
def audio_fsk_ldpc_0(self) -> None:
@ -874,7 +899,6 @@ class RF:
# only take every tenth data point
static.SCATTER = scatterdata[::10]
def calculate_snr(self, freedv: ctypes.c_void_p) -> float:
"""
Ask codec2 for data about the received signal and calculate
@ -908,6 +932,20 @@ class RF:
static.SNR = 0
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:
"""
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_BANDWIDTH = self.hamlib.get_bandwidth()
static.HAMLIB_STATUS = self.hamlib.get_status()
def calculate_fft(self) -> None:
"""
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:
rms = int(np.sqrt(np.max(d ** 2)))
if rms == 0:
raise ZeroDivisionError
static.AUDIO_DBFS = 20 * np.log10(rms / 32768)
except Exception as e:
self.log.warning(
@ -1009,9 +1050,9 @@ class RF:
# so we have a smoother state toggle
if np.sum(dfft[dfft > avg + 15]) >= 400 and not static.TRANSMITTING:
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
channel_busy_delay = min(channel_busy_delay + 10, 250)
channel_busy_delay = min(channel_busy_delay + 10, 200)
else:
# Decrement channel busy counter if no signal has been detected.
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
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):
"""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.port = port
self.connection_attempts = 5
@ -33,7 +35,6 @@ class radio:
self.frequency = ''
self.mode = ''
def open_rig(
self,
devicename,
@ -67,8 +68,20 @@ class radio:
self.hostname = rigctld_ip
self.port = int(rigctld_port)
if self.connect():
self.log.debug("Rigctl initialized")
#_ptt_connect = self.ptt_connect()
#_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
self.log.error(
@ -76,33 +89,58 @@ class radio:
)
return False
def connect(self):
def ptt_connect(self):
"""Connect to rigctld instance"""
if not self.connected:
try:
self.connection = socket.create_connection((self.hostname, self.port))
self.connected = True
self.log.info(
"[RIGCTLD] Connected to rigctld!", ip=self.hostname, port=self.port
)
return True
except Exception as err:
# ConnectionRefusedError: [Errno 111] Connection refused
self.close_rig()
self.log.warning(
"[RIGCTLD] Reconnect...",
ip=self.hostname,
port=self.port,
e=err,
)
return False
while True:
if not self.ptt_connected:
try:
self.ptt_connection = socket.create_connection((self.hostname, self.port))
self.ptt_connected = True
self.log.info(
"[RIGCTLD] Connected PTT 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] PTT Reconnect...",
ip=self.hostname,
port=self.port,
e=err,
)
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):
""" """
self.sock.close()
self.connected = False
self.ptt_sock.close()
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,
and return the return value.
@ -110,9 +148,9 @@ class radio:
command:
"""
if self.connected:
if self.ptt_connected:
try:
self.connection.sendall(command + b"\n")
self.ptt_connection.sendall(command + b"\n")
except Exception:
self.log.warning(
"[RIGCTLD] Command not executed!",
@ -120,12 +158,33 @@ class radio:
ip=self.hostname,
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:
# 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
return self.connection.recv(16) if expect_answer else True
return self.data_connection.recv(64) if expect_answer else True
except Exception:
self.log.warning(
"[RIGCTLD] No command response!",
@ -133,23 +192,17 @@ class radio:
ip=self.hostname,
port=self.port,
)
self.connected = False
else:
# reconnecting....
threading.Event().wait(0.5)
self.connect()
self.data_connected = False
return b""
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):
""" """
try:
data = self.send_command(b"m", True)
data = self.send_data_command(b"m", True)
data = data.split(b"\n")
data = data[0].decode("utf-8")
if 'RPRT' not in data:
@ -165,7 +218,7 @@ class radio:
def get_bandwidth(self):
""" """
try:
data = self.send_command(b"m", True)
data = self.send_data_command(b"m", True)
data = data.split(b"\n")
data = data[1].decode("utf-8")
@ -179,7 +232,7 @@ class radio:
def get_frequency(self):
""" """
try:
data = self.send_command(b"f", True)
data = self.send_data_command(b"f", True)
data = data.decode("utf-8")
if 'RPRT' not in data and data not in [0, '0', '']:
with contextlib.suppress(ValueError):
@ -209,9 +262,39 @@ class radio:
"""
try:
if state:
self.send_command(b"T 1", False)
self.send_ptt_command(b"T 1", False)
else:
self.send_command(b"T 0", False)
self.send_ptt_command(b"T 0", False)
return state
except Exception:
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
def set_bandwidth(self):
""" """
return None
def set_mode(self, mode):
"""
@ -41,6 +44,16 @@ class radio:
"""
return None
def set_frequency(self, frequency):
"""
Args:
mode:
Returns:
"""
return None
def get_status(self):
"""

View file

@ -31,7 +31,7 @@ import static
import structlog
import ujson as json
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()
DAEMON_QUEUE = queue.Queue()
@ -136,7 +136,7 @@ class ThreadedTCPRequestHandler(socketserver.StreamRequestHandler):
# we might improve this by only processing one command or
# doing some kind of selection to determin which commands need to be dropped
# 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
data = bytes()
@ -594,6 +594,32 @@ def process_tnc_commands(data):
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
except Exception as 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)),
"arq_bytes_per_minute": str(static.ARQ_BYTES_PER_MINUTE),
"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_transmission_percent": str(static.ARQ_TRANSMISSION_PERCENT),
"speed_list": static.SPEED_LIST,
"total_bytes": str(static.TOTAL_BYTES),
"beacon_state": str(static.BEACON_STATE),
"stations": [],
"mycallsign": str(static.MYCALLSIGN, encoding),
"mygrid": str(static.MYGRID, encoding),
"dxcallsign": str(static.DXCALLSIGN, encoding),
"dxgrid": str(static.DXGRID, encoding),
"hamlib_status": static.HAMLIB_STATUS,
@ -651,7 +680,6 @@ def send_tnc_state():
"frequency": heard[6],
}
)
return json.dumps(output)

View file

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