mirror of
https://github.com/DJ2LS/FreeDATA
synced 2024-05-14 10:04:33 +02:00
Merge pull request #323 from DJ2LS/ls-arq
Further ARQ improvements + speed chart
This commit is contained in:
commit
3c0484cf46
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
|
17
gui/sock.js
17
gui/sock.js
|
@ -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 = {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -8,6 +8,7 @@ body {
|
|||
/*Progress bars with centered text*/
|
||||
.progress {
|
||||
position: relative;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.progress span {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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.
|
||||
|
|
10
tnc/main.py
10
tnc/main.py
|
@ -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
|
||||
|
||||
|
|
67
tnc/modem.py
67
tnc/modem.py
|
@ -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)
|
||||
|
|
|
@ -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()
|
173
tnc/rigctld.py
173
tnc/rigctld.py
|
@ -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
|
|
@ -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):
|
||||
"""
|
||||
|
||||
|
|
34
tnc/sock.py
34
tnc/sock.py
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue