Merge pull request #311 from DJ2LS/ls-arq

arq improvements from OTA tests
This commit is contained in:
DJ2LS 2022-12-29 09:38:42 +01:00 committed by GitHub
commit 0d13b638ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 532 additions and 197 deletions

View file

@ -268,7 +268,7 @@ jobs:
# if: matrix.os == 'ubuntu-20.04' # if: matrix.os == 'ubuntu-20.04'
if: ${{startsWith(matrix.os, 'ubuntu')}} if: ${{startsWith(matrix.os, 'ubuntu')}}
run: | run: |
sudo apt install -y portaudio19-dev libhamlib-dev libhamlib-utils build-essential cmake python3-libhamlib2 sudo apt install -y portaudio19-dev libhamlib-dev libhamlib-utils build-essential cmake python3-libhamlib2 patchelf
- name: Install MacOS pyAudio - name: Install MacOS pyAudio
if: ${{startsWith(matrix.os, 'macos')}} if: ${{startsWith(matrix.os, 'macos')}}

View file

@ -38,10 +38,13 @@
"electron-updater": "^5.2.1", "electron-updater": "^5.2.1",
"emoji-picker-element": "^1.12.1", "emoji-picker-element": "^1.12.1",
"emoji-picker-element-data": "^1.3.0", "emoji-picker-element-data": "^1.3.0",
"express-pouchdb": "^4.2.0",
"mime": "^3.0.0", "mime": "^3.0.0",
"pouchdb": "^7.3.0", "pouchdb": "^7.3.0",
"pouchdb-browser": "^7.3.0", "pouchdb-browser": "^7.3.0",
"pouchdb-express-router": "^0.0.11",
"pouchdb-find": "^7.3.0", "pouchdb-find": "^7.3.0",
"pouchdb-replication": "^8.0.0",
"qth-locator": "^2.1.0", "qth-locator": "^2.1.0",
"utf8": "^3.0.0", "utf8": "^3.0.0",
"uuid": "^9.0.0" "uuid": "^9.0.0"

View file

@ -34,7 +34,7 @@ const dateFormatHours = new Intl.DateTimeFormat('en-GB', {
hour12: false, hour12: false,
}); });
// split character // split character
const split_char = '\0;' const split_char = '\0;\1;'
// global for our selected file we want to transmit // global for our selected file we want to transmit
// ----------------- some chat globals // ----------------- some chat globals
var filetype = ''; var filetype = '';
@ -61,25 +61,51 @@ try{
} }
PouchDB.plugin(require('pouchdb-find')); PouchDB.plugin(require('pouchdb-find'));
var db = new PouchDB(chatDB); //PouchDB.plugin(require('pouchdb-replication'));
var remoteDB = new PouchDB('http://192.168.178.79:5984/chatDB')
db.sync(remoteDB, { var db = new PouchDB(chatDB);
/*
// REMOTE SYNC ATTEMPTS
var remoteDB = new PouchDB('http://172.20.10.4:5984/chatDB')
// we need express packages for running pouchdb sync "express-pouchdb"
var express = require('express');
var app = express();
//app.use('/chatDB', require('express-pouchdb')(PouchDB));
//app.listen(5984);
app.use('/chatDB', require('pouchdb-express-router')(PouchDB));
app.listen(5984);
db.sync('http://172.20.10.4:5984/jojo', {
//var sync = PouchDB.sync('chatDB', 'http://172.20.10.4:5984/chatDB', {
live: true, live: true,
retry: true retry: false
}).on('change', function (change) { }).on('change', function (change) {
// yo, something changed! // yo, something changed!
console.log(change) console.log(change)
}).on('paused', function (info) { }).on('paused', function (err) {
// replication was paused, usually because of a lost connection // replication was paused, usually because of a lost connection
console.log(info) console.log(err)
}).on('active', function (info) { }).on('active', function (info) {
// replication was resumed // replication was resumed
console.log(info) console.log(info)
}).on('error', function (err) { }).on('error', function (err) {
// totally unhandled error (shouldn't happen) // totally unhandled error (shouldn't happen)
console.log(error) console.log(err)
}).on('denied', function (err) {
// a document failed to replicate (e.g. due to permissions)
console.log(err)
}).on('complete', function (info) {
// handle complete;
console.log(info)
}); });
*/
var dxcallsigns = new Set(); var dxcallsigns = new Set();
db.createIndex({ db.createIndex({
@ -297,7 +323,9 @@ db.post({
} }
var timestamp = Math.floor(Date.now() / 1000); var timestamp = Math.floor(Date.now() / 1000);
var data_with_attachment = chatmessage + split_char + filename + split_char + filetype + split_char + file + split_char + timestamp; var file_checksum = crc32(file).toString(16).toUpperCase();
console.log(file_checksum)
var data_with_attachment = timestamp + split_char + chatmessage + split_char + filename + split_char + filetype + split_char + file;
document.getElementById('selectFilesButton').innerHTML = ``; document.getElementById('selectFilesButton').innerHTML = ``;
var uuid = uuidv4(); var uuid = uuidv4();
@ -308,7 +336,7 @@ db.post({
mode: 255, mode: 255,
frames: 1, frames: 1,
data: data_with_attachment, data: data_with_attachment,
checksum: '123', checksum: file_checksum,
uuid: uuid uuid: uuid
}; };
ipcRenderer.send('run-tnc-command', Data); ipcRenderer.send('run-tnc-command', Data);
@ -318,7 +346,7 @@ db.post({
dxcallsign: dxcallsign, dxcallsign: dxcallsign,
dxgrid: 'null', dxgrid: 'null',
msg: chatmessage, msg: chatmessage,
checksum: 'null', checksum: file_checksum,
type: "transmit", type: "transmit",
status: 'transmit', status: 'transmit',
uuid: uuid, uuid: uuid,
@ -404,7 +432,7 @@ ipcRenderer.on('action-new-msg-received', (event, arg) => {
//handle ping //handle ping
if (item.ping == 'received') { if (item.ping == 'received') {
obj.timestamp = item.timestamp; obj.timestamp = parseInt(item.timestamp);
obj.dxcallsign = item.dxcallsign; obj.dxcallsign = item.dxcallsign;
obj.dxgrid = item.dxgrid; obj.dxgrid = item.dxgrid;
obj.uuid = item.uuid; obj.uuid = item.uuid;
@ -421,12 +449,9 @@ ipcRenderer.on('action-new-msg-received', (event, arg) => {
add_obj_to_database(obj) add_obj_to_database(obj)
update_chat_obj_by_uuid(obj.uuid); update_chat_obj_by_uuid(obj.uuid);
// handle beacon // handle beacon
} else if (item.beacon == 'received') { } else if (item.beacon == 'received') {
obj.timestamp = item.timestamp; obj.timestamp = parseInt(item.timestamp);
obj.dxcallsign = item.dxcallsign; obj.dxcallsign = item.dxcallsign;
obj.dxgrid = item.dxgrid; obj.dxgrid = item.dxgrid;
obj.uuid = item.uuid; obj.uuid = item.uuid;
@ -451,20 +476,20 @@ ipcRenderer.on('action-new-msg-received', (event, arg) => {
console.log(splitted_data) console.log(splitted_data)
obj.timestamp = splitted_data[8]; obj.timestamp = parseInt(splitted_data[4]);
obj.dxcallsign = item.dxcallsign; obj.dxcallsign = item.dxcallsign;
obj.dxgrid = item.dxgrid; obj.dxgrid = item.dxgrid;
obj.command = splitted_data[1]; obj.command = splitted_data[1];
obj.checksum = splitted_data[2]; obj.checksum = splitted_data[2];
// convert message to unicode from utf8 because of emojis // convert message to unicode from utf8 because of emojis
obj.uuid = utf8.decode(splitted_data[3]); obj.uuid = utf8.decode(splitted_data[3]);
obj.msg = utf8.decode(splitted_data[4]); obj.msg = utf8.decode(splitted_data[5]);
obj.status = 'null'; obj.status = 'null';
obj.snr = 'null'; obj.snr = 'null';
obj.type = 'received'; obj.type = 'received';
obj.filename = utf8.decode(splitted_data[5]); obj.filename = utf8.decode(splitted_data[6]);
obj.filetype = utf8.decode(splitted_data[6]); obj.filetype = utf8.decode(splitted_data[7]);
obj.file = btoa(utf8.decode(splitted_data[7])); obj.file = btoa(splitted_data[8]);
add_obj_to_database(obj); add_obj_to_database(obj);
update_chat_obj_by_uuid(obj.uuid); update_chat_obj_by_uuid(obj.uuid);
@ -839,7 +864,9 @@ update_chat = function(obj) {
}).then(function(){ }).then(function(){
console.log(binaryString) console.log(binaryString)
var data_with_attachment = doc.msg + split_char + filename + split_char + filetype + split_char + binaryString + split_char + doc.timestamp; console.log(binaryString.length)
var data_with_attachment = doc.timestamp + split_char + utf8.encode(doc.msg) + split_char + filename + split_char + filetype + split_char + binaryString;
let Data = { let Data = {
command: "send_message", command: "send_message",
dxcallsign: doc.dxcallsign, dxcallsign: doc.dxcallsign,
@ -939,7 +966,7 @@ update_chat_obj_by_uuid = function(uuid) {
add_obj_to_database = function(obj){ add_obj_to_database = function(obj){
db.put({ db.put({
_id: obj.uuid, _id: obj.uuid,
timestamp: obj.timestamp, timestamp: parseInt(obj.timestamp),
uuid: obj.uuid, uuid: obj.uuid,
dxcallsign: obj.dxcallsign, dxcallsign: obj.dxcallsign,
dxgrid: obj.dxgrid, dxgrid: obj.dxgrid,
@ -969,3 +996,36 @@ function scrollMessagesToBottom() {
var messageBody = document.getElementById('message-container'); var messageBody = document.getElementById('message-container');
messageBody.scrollTop = messageBody.scrollHeight - messageBody.clientHeight; messageBody.scrollTop = messageBody.scrollHeight - messageBody.clientHeight;
} }
// CRC CHECKSUMS
// https://stackoverflow.com/a/50579690
// crc32 calculation
//console.log(crc32('abc'));
//var crc32=function(r){for(var a,o=[],c=0;c<256;c++){a=c;for(var f=0;f<8;f++)a=1&a?3988292384^a>>>1:a>>>1;o[c]=a}for(var n=-1,t=0;t<r.length;t++)n=n>>>8^o[255&(n^r.charCodeAt(t))];return(-1^n)>>>0};
//console.log(crc32('abc').toString(16).toUpperCase()); // hex
var makeCRCTable = function(){
var c;
var crcTable = [];
for(var n =0; n < 256; n++){
c = n;
for(var k =0; k < 8; k++){
c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
}
crcTable[n] = c;
}
return crcTable;
}
var crc32 = function(str) {
var crcTable = window.crcTable || (window.crcTable = makeCRCTable());
var crc = 0 ^ (-1);
for (var i = 0; i < str.length; i++ ) {
crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xFF];
}
return (crc ^ (-1)) >>> 0;
};

View file

@ -38,6 +38,19 @@ var dbfs_level_raw = 0
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
// start stop audio recording event listener
document.getElementById("startStopRecording").addEventListener("click", () => {
let Data = {
type: "set",
command: "record_audio",
};
ipcRenderer.send('run-tnc-command', Data);
});
document.getElementById('received_files_folder').addEventListener('click', () => { document.getElementById('received_files_folder').addEventListener('click', () => {
ipcRenderer.send('get-folder-path',{ ipcRenderer.send('get-folder-path',{
@ -96,8 +109,8 @@ document.getElementById('openReceivedFilesFolder').addEventListener('click', ()
// hamlib settings // hamlib settings
document.getElementById('hamlib_deviceid').value = config.hamlib_deviceid; document.getElementById('hamlib_deviceid').value = config.hamlib_deviceid;
set_setting_switch("enable_hamlib_deviceport", "hamlib_deviceport", config.enable_hamlib_deviceport) set_setting_switch("enable_hamlib_deviceport", "hamlib_deviceport", config.enable_hamlib_deviceport)
set_setting_switch("enable_hamlib_ptt_port", "hamlib_ptt_port", config.enable_hamlib_ptt_port) set_setting_switch("enable_hamlib_ptt_port", "hamlib_ptt_port", config.enable_hamlib_ptt_port)
document.getElementById('hamlib_serialspeed').value = config.hamlib_serialspeed; document.getElementById('hamlib_serialspeed').value = config.hamlib_serialspeed;
set_setting_switch("enable_hamlib_serialspeed", "hamlib_serialspeed", config.enable_hamlib_serialspeed) set_setting_switch("enable_hamlib_serialspeed", "hamlib_serialspeed", config.enable_hamlib_serialspeed)
@ -1441,6 +1454,22 @@ ipcRenderer.on('action-update-tnc-state', (event, arg) => {
document.getElementById("ptt_state").className = "btn btn-sm btn-secondary"; document.getElementById("ptt_state").className = "btn btn-sm btn-secondary";
} }
// AUDIO RECORDING
if (arg.audio_recording == 'True') {
document.getElementById("startStopRecording").className = "btn btn-sm btn-danger";
document.getElementById("startStopRecording").innerHTML = "Stop Rec"
} else if (arg.ptt_state == 'False') {
document.getElementById("startStopRecording").className = "btn btn-sm btn-danger";
document.getElementById("startStopRecording").innerHTML = "Start Rec"
} else {
document.getElementById("startStopRecording").className = "btn btn-sm btn-danger";
document.getElementById("startStopRecording").innerHTML = "Start Rec"
}
// CHANNEL BUSY STATE // CHANNEL BUSY STATE
if (arg.channel_busy == 'True') { if (arg.channel_busy == 'True') {
document.getElementById("channel_busy").className = "btn btn-sm btn-danger"; document.getElementById("channel_busy").className = "btn btn-sm btn-danger";
@ -2075,6 +2104,9 @@ ipcRenderer.on('run-tnc-command', (event, arg) => {
if (arg.command == 'set_tx_audio_level') { if (arg.command == 'set_tx_audio_level') {
sock.setTxAudioLevel(arg.tx_audio_level); sock.setTxAudioLevel(arg.tx_audio_level);
} }
if (arg.command == 'record_audio') {
sock.record_audio();
}
if (arg.command == 'send_test_frame') { if (arg.command == 'send_test_frame') {
sock.sendTestFrame(); sock.sendTestFrame();
} }

View file

@ -19,7 +19,7 @@ var client = new net.Socket();
var socketchunk = ''; // Current message, per connection. var socketchunk = ''; // Current message, per connection.
// split character // split character
const split_char = '\0;' const split_char = '\0;\1;'
// globals for getting new data only if available so we are saving bandwidth // globals for getting new data only if available so we are saving bandwidth
var rxBufferLengthTnc = 0 var rxBufferLengthTnc = 0
@ -227,6 +227,8 @@ client.on('data', function(socketdata) {
stations: data['stations'], stations: data['stations'],
beacon_state: data['beacon_state'], beacon_state: data['beacon_state'],
hamlib_status: data['hamlib_status'], hamlib_status: data['hamlib_status'],
listen: data['listen'],
audio_recording: data['audio_recording'],
}; };
ipcRenderer.send('request-update-tnc-state', Data); ipcRenderer.send('request-update-tnc-state', Data);
@ -515,20 +517,25 @@ exports.sendFile = function(dxcallsign, mode, frames, filename, filetype, data,
// Send Message // Send Message
exports.sendMessage = function(dxcallsign, mode, frames, data, checksum, uuid, command) { exports.sendMessage = function(dxcallsign, mode, frames, data, checksum, uuid, command) {
socketLog.info(data) //socketLog.info(data)
// Disabled this here
// convert message to plain utf8 because of unicode emojis // convert message to plain utf8 because of unicode emojis
data = utf8.encode(data) //data = utf8.encode(data)
socketLog.info(data)
//socketLog.info(data)
var datatype = "m" var datatype = "m"
data = datatype + split_char + command + split_char + checksum + split_char + uuid + split_char + data data = datatype + split_char + command + split_char + checksum + split_char + uuid + split_char + data
socketLog.info(data) //socketLog.info(data)
console.log(data)
console.log("CHECKSUM" + checksum)
socketLog.info(btoa(data)) //socketLog.info(btoa(data))
data = btoa(data) data = btoa(data)
//command = '{"type" : "arq", "command" : "send_message", "parameter" : [{ "dxcallsign" : "' + dxcallsign + '", "mode" : "' + mode + '", "n_frames" : "' + frames + '", "data" : "' + data + '" , "checksum" : "' + checksum + '"}]}' //command = '{"type" : "arq", "command" : "send_message", "parameter" : [{ "dxcallsign" : "' + dxcallsign + '", "mode" : "' + mode + '", "n_frames" : "' + frames + '", "data" : "' + data + '" , "checksum" : "' + checksum + '"}]}'
command = '{"type" : "arq", "command" : "send_raw", "uuid" : "'+ uuid +'", "parameter" : [{"dxcallsign" : "' + dxcallsign + '", "mode" : "' + mode + '", "n_frames" : "' + frames + '", "data" : "' + data + '", "attempts": "15"}]}' command = '{"type" : "arq", "command" : "send_raw", "uuid" : "'+ uuid +'", "parameter" : [{"dxcallsign" : "' + dxcallsign + '", "mode" : "' + mode + '", "n_frames" : "' + frames + '", "data" : "' + data + '", "attempts": "15"}]}'
socketLog.info(command) socketLog.info(command)
@ -584,7 +591,11 @@ exports.sendTestFrame = function() {
writeTncCommand(command) writeTncCommand(command)
} }
// RECORD AUDIO
exports.record_audio = function() {
command = '{"type" : "set", "command" : "record_audio"}'
writeTncCommand(command)
}
ipcRenderer.on('action-update-tnc-ip', (event, arg) => { ipcRenderer.on('action-update-tnc-ip', (event, arg) => {
client.destroy(); client.destroy();
@ -602,4 +613,12 @@ ipcRenderer.on('action-update-tnc-ip', (event, arg) => {
tnc_host = arg.adress; tnc_host = arg.adress;
connectTNC(); connectTNC();
}); });
// https://stackoverflow.com/a/50579690
// crc32 calculation
//console.log(crc32('abc'));
//console.log(crc32('abc').toString(16).toUpperCase()); // hex
var crc32=function(r){for(var a,o=[],c=0;c<256;c++){a=c;for(var f=0;f<8;f++)a=1&a?3988292384^a>>>1:a>>>1;o[c]=a}for(var n=-1,t=0;t<r.length;t++)n=n>>>8^o[255&(n^r.charCodeAt(t))];return(-1^n)>>>0};

View file

@ -768,6 +768,8 @@
<div class="card text-dark mb-1"> <div class="card text-dark mb-1">
<div class="card-header p-1"><i class="bi bi-volume-up" style="font-size: 1rem; color: black;"></i> <strong>AUDIO LEVEL</strong> <div class="card-header p-1"><i class="bi bi-volume-up" style="font-size: 1rem; color: black;"></i> <strong>AUDIO LEVEL</strong>
<button type="button" id="audioModalButton" data-bs-toggle="modal" data-bs-target="#audioModal" class="btn btn-sm btn-secondary">Tune</button> <button type="button" id="audioModalButton" data-bs-toggle="modal" data-bs-target="#audioModal" class="btn btn-sm btn-secondary">Tune</button>
<button type="button" id="startStopRecording" class="btn btn-sm btn-danger">Rec</button>
</div> </div>
<div class="card-body p-2"> <div class="card-body p-2">
<div class="progress mb-0" style="height: 15px;"> <div class="progress mb-0" style="height: 15px;">

View file

@ -296,6 +296,6 @@ def t_datac0_2(
assert item in str( assert item in str(
sock.SOCKET_QUEUE.queue sock.SOCKET_QUEUE.queue
), f"{item} not found in {str(sock.SOCKET_QUEUE.queue)}" ), f"{item} not found in {str(sock.SOCKET_QUEUE.queue)}"
# TODO: Not sure why we need this for every test run
assert '"arq":"session","status":"close"' in str(sock.SOCKET_QUEUE.queue) # assert '"arq":"session","status":"close"' in str(sock.SOCKET_QUEUE.queue)
log.warning("station2: Exiting!") log.warning("station2: Exiting!")

View file

@ -305,6 +305,6 @@ def t_datac0_2(
assert item not in str( assert item not in str(
sock.SOCKET_QUEUE.queue sock.SOCKET_QUEUE.queue
), f"{item} found in {str(sock.SOCKET_QUEUE.queue)}" ), f"{item} found in {str(sock.SOCKET_QUEUE.queue)}"
# TODO: Not sure why we need this for every test run
assert '"arq":"session","status":"close"' in str(sock.SOCKET_QUEUE.queue) # assert '"arq":"session","status":"close"' in str(sock.SOCKET_QUEUE.queue)
log.warning("station2: Exiting!") log.warning("station2: Exiting!")

View file

@ -7,6 +7,7 @@ Created on Sun Dec 27 20:43:40 2020
# pylint: disable=invalid-name, line-too-long, c-extension-no-member # pylint: disable=invalid-name, line-too-long, c-extension-no-member
# pylint: disable=import-outside-toplevel, attribute-defined-outside-init # pylint: disable=import-outside-toplevel, attribute-defined-outside-init
import os
import base64 import base64
import sys import sys
import threading import threading
@ -80,8 +81,8 @@ class DATA:
# 3 bytes for the EOF End of File indicator in a data frame # 3 bytes for the EOF End of File indicator in a data frame
self.data_frame_eof = b"EOF" self.data_frame_eof = b"EOF"
self.tx_n_max_retries_per_burst = 50 self.tx_n_max_retries_per_burst = 40
self.rx_n_max_retries_per_burst = 50 self.rx_n_max_retries_per_burst = 40
self.n_retries_per_burst = 0 self.n_retries_per_burst = 0
# Flag to indicate if we recevied a low bandwidth mode channel opener # Flag to indicate if we recevied a low bandwidth mode channel opener
@ -590,7 +591,8 @@ class DATA:
mycallsign = self.mycallsign mycallsign = self.mycallsign
# only process data if we are in ARQ and BUSY state else return to quit # only process data if we are in ARQ and BUSY state else return to quit
if not static.ARQ_STATE and static.TNC_STATE != "BUSY": if not static.ARQ_STATE and static.TNC_STATE not in ["BUSY"]:
self.log.warning("[TNC] wrong tnc state - dropping data", arq_state=static.ARQ_STATE, tnc_state=static.TNC_STATE)
return return
self.arq_file_transfer = True self.arq_file_transfer = True
@ -810,7 +812,7 @@ class DATA:
# transmittion duration # transmittion duration
duration = time.time() - self.rx_start_of_transmission duration = time.time() - self.rx_start_of_transmission
self.log.info("[TNC] ARQ | RX | DATA FRAME SUCCESSFULLY RECEIVED", nacks=self.frame_nack_counter,bytesperminute=static.ARQ_BYTES_PER_MINUTE, duration=duration self.log.info("[TNC] ARQ | RX | DATA FRAME SUCCESSFULLY RECEIVED", nacks=self.frame_nack_counter,bytesperminute=static.ARQ_BYTES_PER_MINUTE, total_bytes=static.TOTAL_BYTES, duration=duration
) )
# Decompress the data frame # Decompress the data frame
@ -864,13 +866,33 @@ class DATA:
self.log.error( self.log.error(
"[TNC] ARQ | RX | error occurred when saving data!", "[TNC] ARQ | RX | error occurred when saving data!",
e=e, e=e,
uuid = self.transmission_uuid, uuid=self.transmission_uuid,
timestamp = timestamp, timestamp=timestamp,
dxcall = static.DXCALLSIGN, dxcall=static.DXCALLSIGN,
dxgrid = static.DXGRID, dxgrid=static.DXGRID,
data = base64_data data=base64_data
) )
if static.ARQ_SAVE_TO_FOLDER:
try:
self.save_data_to_folder(
self.transmission_uuid,
timestamp,
mycallsign,
static.DXCALLSIGN,
static.DXGRID,
data_frame
)
except Exception as e:
self.log.error(
"[TNC] ARQ | RX | can't save file to folder",
e=e,
uuid=self.transmission_uuid,
timestamp=timestamp,
dxcall=static.DXCALLSIGN,
dxgrid=static.DXGRID,
data=base64_data
)
self.send_data_to_socket_queue( self.send_data_to_socket_queue(
freedata="tnc-message", freedata="tnc-message",
arq="transmission", arq="transmission",
@ -1194,6 +1216,7 @@ class DATA:
self.log.info( self.log.info(
"[TNC] ARQ | TX | DATA TRANSMITTED!", "[TNC] ARQ | TX | DATA TRANSMITTED!",
BytesPerMinute=static.ARQ_BYTES_PER_MINUTE, BytesPerMinute=static.ARQ_BYTES_PER_MINUTE,
total_bytes=static.TOTAL_BYTES,
BitsPerSecond=static.ARQ_BITS_PER_SECOND, BitsPerSecond=static.ARQ_BITS_PER_SECOND,
overflows=static.BUFFER_OVERFLOW_COUNTER, overflows=static.BUFFER_OVERFLOW_COUNTER,
@ -1512,7 +1535,7 @@ class DATA:
+ "]>>?<<[" + "]>>?<<["
+ str(self.dxcallsign, "UTF-8") + str(self.dxcallsign, "UTF-8")
+ "]", + "]",
a=str(attempt + 1) + "/" + str(self.session_connect_max_retries), # Adjust for 0-based for user display a=f"{str(attempt + 1)}/{str(self.session_connect_max_retries)}",
state=static.ARQ_SESSION_STATE, state=static.ARQ_SESSION_STATE,
) )
@ -1818,37 +1841,6 @@ class DATA:
# for calculating transmission statistics # for calculating transmission statistics
# static.ARQ_COMPRESSION_FACTOR = len(data_out) / len(lzma.compress(data_out)) # static.ARQ_COMPRESSION_FACTOR = len(data_out) / len(lzma.compress(data_out))
# Let's check if we have a busy channel and if we are not in a running arq session.
if static.CHANNEL_BUSY and not static.ARQ_SESSION:
self.log.warning("[TNC] Channel busy, waiting until free...")
self.send_data_to_socket_queue(
freedata="tnc-message",
arq="transmission",
status="waiting",
mycallsign=str(self.mycallsign, 'UTF-8'),
dxcallsign=str(self.dxcallsign, 'UTF-8'),
)
# wait while timeout not reached and our busy state is busy
channel_busy_timeout = time.time() + 30
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.arq_open_data_channel(mode, n_frames_per_burst, mycallsign) self.arq_open_data_channel(mode, n_frames_per_burst, mycallsign)
# wait until data channel is open # wait until data channel is open
@ -1923,6 +1915,37 @@ class DATA:
attempt=f"{str(attempt + 1)}/{str(self.data_channel_max_retries)}", attempt=f"{str(attempt + 1)}/{str(self.data_channel_max_retries)}",
) )
# Let's check if we have a busy channel and if we are not in a running arq session.
if static.CHANNEL_BUSY and not static.ARQ_STATE:
self.log.warning("[TNC] Channel busy, waiting until free...")
self.send_data_to_socket_queue(
freedata="tnc-message",
arq="transmission",
status="waiting",
mycallsign=str(self.mycallsign, 'UTF-8'),
dxcallsign=str(self.dxcallsign, 'UTF-8'),
)
# wait while timeout not reached and our busy state is busy
channel_busy_timeout = time.time() + 30
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) self.enqueue_frame_for_tx([connection_frame], c2_mode=FREEDV_MODE.datac0.value, copies=1, repeat_delay=0)
timeout = time.time() + 3 timeout = time.time() + 3
@ -1931,6 +1954,8 @@ class DATA:
# Stop waiting if data channel is opened # Stop waiting if data channel is opened
if static.ARQ_STATE: if static.ARQ_STATE:
return True return True
if static.TNC_STATE in ["IDLE"]:
return False
# `data_channel_max_retries` attempts have been sent. Aborting attempt & cleaning up # `data_channel_max_retries` attempts have been sent. Aborting attempt & cleaning up
@ -2214,6 +2239,7 @@ class DATA:
received=protocol_version, received=protocol_version,
own=static.ARQ_PROTOCOL_VERSION, own=static.ARQ_PROTOCOL_VERSION,
) )
self.stop_transmission()
self.arq_cleanup() self.arq_cleanup()
# ---------- PING # ---------- PING
@ -2382,6 +2408,18 @@ class DATA:
Force a stop of the running transmission Force a stop of the running transmission
""" """
self.log.warning("[TNC] Stopping transmission!") self.log.warning("[TNC] Stopping transmission!")
static.TNC_STATE = "IDLE"
static.ARQ_STATE = False
self.send_data_to_socket_queue(
freedata="tnc-message",
arq="transmission",
status="stopped",
mycallsign=str(self.mycallsign, 'UTF-8'),
dxcallsign=str(self.dxcallsign, 'UTF-8'),
)
stop_frame = bytearray(self.length_sig0_frame) stop_frame = bytearray(self.length_sig0_frame)
stop_frame[:1] = bytes([FR_TYPE.ARQ_STOP.value]) stop_frame[:1] = bytes([FR_TYPE.ARQ_STOP.value])
stop_frame[1:4] = static.DXCALLSIGN_CRC stop_frame[1:4] = static.DXCALLSIGN_CRC
@ -2389,17 +2427,9 @@ class DATA:
# TODO: Not sure if we really need the session id when disconnecting # TODO: Not sure if we really need the session id when disconnecting
# stop_frame[1:2] = self.session_id # stop_frame[1:2] = self.session_id
stop_frame[7:13] = helpers.callsign_to_bytes(self.mycallsign) stop_frame[7:13] = helpers.callsign_to_bytes(self.mycallsign)
self.enqueue_frame_for_tx([stop_frame], c2_mode=FREEDV_MODE.sig1.value, copies=6, repeat_delay=0) self.enqueue_frame_for_tx([stop_frame], c2_mode=FREEDV_MODE.sig1.value, copies=6, repeat_delay=0)
static.TNC_STATE = "IDLE"
static.ARQ_STATE = False
self.send_data_to_socket_queue(
freedata="tnc-message",
arq="transmission",
mycallsign=str(self.mycallsign, 'UTF-8'),
dxcallsign=str(self.dxcallsign, 'UTF-8'),
status="stopped",
)
self.arq_cleanup() self.arq_cleanup()
def received_stop_transmission( def received_stop_transmission(
@ -2414,9 +2444,9 @@ class DATA:
self.send_data_to_socket_queue( self.send_data_to_socket_queue(
freedata="tnc-message", freedata="tnc-message",
arq="transmission", arq="transmission",
status="stopped",
mycallsign=str(self.mycallsign, 'UTF-8'), mycallsign=str(self.mycallsign, 'UTF-8'),
dxcallsign=str(self.dxcallsign, 'UTF-8'), dxcallsign=str(self.dxcallsign, 'UTF-8'),
status="stopped",
uuid=self.transmission_uuid, uuid=self.transmission_uuid,
) )
self.arq_cleanup() self.arq_cleanup()
@ -2440,6 +2470,9 @@ class DATA:
not static.ARQ_SESSION not static.ARQ_SESSION
and not self.arq_file_transfer and not self.arq_file_transfer
and not static.BEACON_PAUSE and not static.BEACON_PAUSE
and not static.CHANNEL_BUSY
and static.TNC_STATE not in ["busy"]
and not static.ARQ_STATE
): ):
self.send_data_to_socket_queue( self.send_data_to_socket_queue(
freedata="tnc-message", freedata="tnc-message",
@ -2454,7 +2487,7 @@ class DATA:
beacon_frame = bytearray(self.length_sig0_frame) beacon_frame = bytearray(self.length_sig0_frame)
beacon_frame[:1] = bytes([FR_TYPE.BEACON.value]) beacon_frame[:1] = bytes([FR_TYPE.BEACON.value])
beacon_frame[1:7] = helpers.callsign_to_bytes(self.mycallsign) beacon_frame[1:7] = helpers.callsign_to_bytes(self.mycallsign)
beacon_frame[9:13] = static.MYGRID[:4] beacon_frame[7:13] = static.MYGRID
self.log.info("[TNC] ENABLE FSK", state=static.ENABLE_FSK) self.log.info("[TNC] ENABLE FSK", state=static.ENABLE_FSK)
if static.ENABLE_FSK: if static.ENABLE_FSK:
@ -2486,7 +2519,7 @@ class DATA:
""" """
# here we add the received station to the heard stations buffer # here we add the received station to the heard stations buffer
beacon_callsign = helpers.bytes_to_callsign(bytes(data_in[1:7])) beacon_callsign = helpers.bytes_to_callsign(bytes(data_in[1:7]))
dxgrid = bytes(data_in[9:13]).rstrip(b"\x00") dxgrid = bytes(data_in[7:13]).rstrip(b"\x00")
self.send_data_to_socket_queue( self.send_data_to_socket_queue(
freedata="tnc-message", freedata="tnc-message",
@ -2812,8 +2845,8 @@ class DATA:
self.n_retries_per_burst = 0 self.n_retries_per_burst = 0
# reset max retries possibly overriden by api # reset max retries possibly overriden by api
self.session_connect_max_retries = 15 self.session_connect_max_retries = 10
self.data_channel_max_retries = 15 self.data_channel_max_retries = 10
if not static.ARQ_SESSION: if not static.ARQ_SESSION:
static.TNC_STATE = "IDLE" static.TNC_STATE = "IDLE"
@ -2938,7 +2971,8 @@ class DATA:
self.send_burst_nack_frame_watchdog(0) self.send_burst_nack_frame_watchdog(0)
# Update data_channel timestamp # Update data_channel timestamp
self.data_channel_last_received = time.time() # TODO: Disabled this one for testing.
# self.data_channel_last_received = time.time()
self.n_retries_per_burst += 1 self.n_retries_per_burst += 1
else: else:
# print((self.data_channel_last_received + self.time_list[self.speed_level])-time.time()) # print((self.data_channel_last_received + self.time_list[self.speed_level])-time.time())
@ -3040,3 +3074,81 @@ class DATA:
self.enqueue_frame_for_tx( self.enqueue_frame_for_tx(
frame_to_tx=[bytearray(126)], c2_mode=FREEDV_MODE.datac3.value frame_to_tx=[bytearray(126)], c2_mode=FREEDV_MODE.datac3.value
) )
def save_data_to_folder(self,
transmission_uuid,
timestamp,
mycallsign,
dxcallsign,
dxgrid,
data_frame
):
"""
Save received data to folder
Also supports chat messages
"""
try:
self.log.info("[TNC] ARQ | RX | saving data to folder")
mycallsign = str(mycallsign, "UTF-8")
dxcallsign = str(dxcallsign, "UTF-8")
folder_path = "received"
if not os.path.exists(folder_path):
os.makedirs(folder_path)
callsign_path = f"{mycallsign}_{dxcallsign}"
if not os.path.exists(f"{folder_path}/{callsign_path}"):
os.makedirs(f"{folder_path}/{callsign_path}")
split_char = b"\0;\1;"
n_objects = 9
decoded_data = data_frame.split(split_char)
# if we have a false positive in case our split_char is available in data
# lets stick the data together, so we are not loosing it
if len(decoded_data) > n_objects:
file_data = b''.join(decoded_data[n_objects:])
# slice is crashing nuitka
# decoded_data = [*decoded_data[:n_objects], file_data]
decoded_data = decoded_data[:n_objects] + [file_data]
if decoded_data[0] in [b'm']:
checksum_delivered = str(decoded_data[2], "utf-8").lower()
# transmission_uuid = decoded_data[3]
message = decoded_data[5]
filename = decoded_data[6]
# filetype = decoded_data[7]
# timestamp = decoded_data[4]
data = decoded_data[8]
else:
message = b''
filename = b''
# save file to folder
if filename not in [b'', b'undefined']:
# doing crc check
crc = helpers.get_crc_32(data).hex().lower()
validity = checksum_delivered == crc
self.log.info(
"[TNC] ARQ | RX | checking data crc",
crc_delivered=checksum_delivered,
crc_calculated=crc,
valid=validity,
)
filename = str(filename, "UTF-8")
filename_complex = f"{timestamp}_{transmission_uuid}_{filename}"
with open(f"{folder_path}/{callsign_path}/{filename_complex}", "wb") as file:
file.write(data)
if message not in [b'', b'undefined']:
# save message to folder
message_name = f"{timestamp}_{transmission_uuid}_msg.txt"
with open(f"{folder_path}/{callsign_path}/{message_name}", "wb") as file:
file.write(message)
except Exception as e:
self.log.error("[TNC] error saving data to folder", e=e)

View file

@ -32,11 +32,7 @@ class explorer():
def push(self): def push(self):
frequency = 0 if static.HAMLIB_FREQUENCY is None else static.HAMLIB_FREQUENCY
if static.HAMLIB_FREQUENCY is not None:
frequency = static.HAMLIB_FREQUENCY
else:
frequency = 0
band = "USB" band = "USB"
callsign = str(static.MYCALLSIGN, "utf-8") callsign = str(static.MYCALLSIGN, "utf-8")
gridsquare = str(static.MYGRID, "utf-8") gridsquare = str(static.MYGRID, "utf-8")
@ -47,7 +43,20 @@ class explorer():
log.info("[EXPLORER] publish", frequency=frequency, band=band, callsign=callsign, gridsquare=gridsquare, version=version, bandwidth=bandwidth) log.info("[EXPLORER] publish", frequency=frequency, band=band, callsign=callsign, gridsquare=gridsquare, version=version, bandwidth=bandwidth)
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
station_data = {'callsign': callsign, 'gridsquare': gridsquare, 'frequency': frequency, 'band': band, 'version': version, 'bandwidth': bandwidth, 'beacon': beacon} station_data = {'callsign': callsign, 'gridsquare': gridsquare, 'frequency': frequency, 'band': band, 'version': version, 'bandwidth': bandwidth, 'beacon': beacon, "lastheard": []}
for i in static.HEARD_STATIONS:
try:
callsign = str(i[0], "UTF-8")
grid = str(i[1], "UTF-8")
try:
snr = i[4].split("/")[1]
except AttributeError:
snr = str(i[4])
station_data["lastheard"].append({"callsign": callsign, "grid": grid, "snr": snr})
except Exception as e:
log.debug("[EXPLORER] not publishing station", e=e)
station_data = json.dumps(station_data) station_data = json.dumps(station_data)
try: try:
response = requests.post(self.explorer_url, json=station_data, headers=headers) response = requests.post(self.explorer_url, json=station_data, headers=headers)

View file

@ -331,6 +331,8 @@ def check_session_id(id: bytes, id_to_check: bytes):
True True
False False
""" """
if id_to_check == b'\x00':
return False
log.debug("[HLP] check_sessionid: Checking:", ownid=id, check=id_to_check) log.debug("[HLP] check_sessionid: Checking:", ownid=id, check=id_to_check)
return id == id_to_check return id == id_to_check
@ -392,11 +394,7 @@ def decode_grid(b_code_word: bytes):
int_val = int(code_word & 0b111111111) int_val = int(code_word & 0b111111111)
int_first, int_sec = divmod(int_val, 18) int_first, int_sec = divmod(int_val, 18)
# int_first = int_val // 18 return chr(int(int_first) + 65) + chr(int(int_sec) + 65) + grid
# int_sec = int_val % 18
grid = chr(int(int_first) + 65) + chr(int(int_sec) + 65) + grid
return grid
def encode_call(call): def encode_call(call):

View file

@ -70,6 +70,15 @@ if __name__ == "__main__":
type=str, type=str,
help="Use the default config file config.ini", help="Use the default config file config.ini",
) )
PARSER.add_argument(
"--save-to-folder",
dest="savetofolder",
default=False,
action="store_true",
help="Save received data to local folder",
)
PARSER.add_argument( PARSER.add_argument(
"--mycall", "--mycall",
dest="mycall", dest="mycall",
@ -269,8 +278,9 @@ if __name__ == "__main__":
) )
ARGS = PARSER.parse_args() ARGS = PARSER.parse_args()
# set save to folder state for allowing downloading files to local file system
static.ARQ_SAVE_TO_FOLDER = ARGS.savetofolder
if not ARGS.configfile: if not ARGS.configfile:

View file

@ -5,6 +5,7 @@ Created on Wed Dec 23 07:04:24 2020
@author: DJ2LS @author: DJ2LS
""" """
# pylint: disable=invalid-name, line-too-long, c-extension-no-member # pylint: disable=invalid-name, line-too-long, c-extension-no-member
# pylint: disable=import-outside-toplevel # pylint: disable=import-outside-toplevel
@ -15,8 +16,9 @@ import sys
import threading import threading
import time import time
from collections import deque from collections import deque
import wave
import codec2 import codec2
import itertools
import numpy as np import numpy as np
import sock import sock
import sounddevice as sd import sounddevice as sd
@ -67,6 +69,7 @@ class RF:
self.AUDIO_CHANNELS = 1 self.AUDIO_CHANNELS = 1
self.MODE = 0 self.MODE = 0
# Locking state for mod out so buffer will be filled before we can use it # Locking state for mod out so buffer will be filled before we can use it
# https://github.com/DJ2LS/FreeDATA/issues/127 # https://github.com/DJ2LS/FreeDATA/issues/127
# https://github.com/DJ2LS/FreeDATA/issues/99 # https://github.com/DJ2LS/FreeDATA/issues/99
@ -355,30 +358,44 @@ class RF:
x = np.frombuffer(data_in48k, dtype=np.int16) x = np.frombuffer(data_in48k, dtype=np.int16)
x = self.resampler.resample48_to_8(x) x = self.resampler.resample48_to_8(x)
# audio recording for debugging purposes
if static.AUDIO_RECORD:
#static.AUDIO_RECORD_FILE.write(x)
static.AUDIO_RECORD_FILE.writeframes(x)
# Avoid decoding when transmitting to reduce CPU # Avoid decoding when transmitting to reduce CPU
if not static.TRANSMITTING: # TODO: Overriding this for testing purposes
length_x = len(x) # if not static.TRANSMITTING:
length_x = len(x)
# Avoid buffer overflow by filling only if buffer for # Avoid buffer overflow by filling only if buffer for
# selected datachannel mode is not full # selected datachannel mode is not full
for audiobuffer, receive, index in [ for audiobuffer, receive, index in [
(self.sig0_datac0_buffer, RECEIVE_SIG0, 0), (self.sig0_datac0_buffer, RECEIVE_SIG0, 0),
(self.sig1_datac0_buffer, RECEIVE_SIG1, 1), (self.sig1_datac0_buffer, RECEIVE_SIG1, 1),
(self.dat0_datac1_buffer, RECEIVE_DATAC1, 2), (self.dat0_datac1_buffer, RECEIVE_DATAC1, 2),
(self.dat0_datac3_buffer, RECEIVE_DATAC3, 3), (self.dat0_datac3_buffer, RECEIVE_DATAC3, 3),
(self.fsk_ldpc_buffer_0, static.ENABLE_FSK, 4), (self.fsk_ldpc_buffer_0, static.ENABLE_FSK, 4),
(self.fsk_ldpc_buffer_1, static.ENABLE_FSK, 5), (self.fsk_ldpc_buffer_1, static.ENABLE_FSK, 5),
]: ]:
if audiobuffer.nbuffer + length_x > audiobuffer.size: if audiobuffer.nbuffer + length_x > audiobuffer.size:
static.BUFFER_OVERFLOW_COUNTER[index] += 1 static.BUFFER_OVERFLOW_COUNTER[index] += 1
elif receive: elif receive:
audiobuffer.push(x) audiobuffer.push(x)
# end of "not static.TRANSMITTING" if block
if len(self.modoutqueue) <= 0 or self.mod_out_locked: if not self.modoutqueue or self.mod_out_locked:
# if not self.modoutqueue or self.mod_out_locked:
data_out48k = np.zeros(frames, dtype=np.int16) data_out48k = np.zeros(frames, dtype=np.int16)
self.fft_data = x self.fft_data = x
else: else:
if not static.PTT_STATE:
# TODO: Moved to this place for testing
# Maybe we can avoid moments of silence before transmitting
static.PTT_STATE = self.hamlib.set_ptt(True)
jsondata = {"ptt": "True"}
data_out = json.dumps(jsondata)
sock.SOCKET_QUEUE.put(data_out)
data_out48k = self.modoutqueue.popleft() data_out48k = self.modoutqueue.popleft()
self.fft_data = data_out48k self.fft_data = data_out48k
@ -424,11 +441,12 @@ class RF:
static.TRANSMITTING = True static.TRANSMITTING = True
start_of_transmission = time.time() start_of_transmission = time.time()
# TODO: Moved ptt toggle some steps before audio is ready for testing
# Toggle ptt early to save some time and send ptt state via socket # Toggle ptt early to save some time and send ptt state via socket
static.PTT_STATE = self.hamlib.set_ptt(True) # static.PTT_STATE = self.hamlib.set_ptt(True)
jsondata = {"ptt": "True"} # jsondata = {"ptt": "True"}
data_out = json.dumps(jsondata) # data_out = json.dumps(jsondata)
sock.SOCKET_QUEUE.put(data_out) # sock.SOCKET_QUEUE.put(data_out)
# Open codec2 instance # Open codec2 instance
self.MODE = mode self.MODE = mode
@ -456,11 +474,12 @@ class RF:
) )
# Add empty data to handle ptt toggle time # Add empty data to handle ptt toggle time
data_delay_mseconds = 0 # milliseconds #data_delay_mseconds = 0 # milliseconds
data_delay = int(self.MODEM_SAMPLE_RATE * (data_delay_mseconds / 1000)) # type: ignore #data_delay = int(self.MODEM_SAMPLE_RATE * (data_delay_mseconds / 1000)) # type: ignore
mod_out_silence = ctypes.create_string_buffer(data_delay * 2) #mod_out_silence = ctypes.create_string_buffer(data_delay * 2)
txbuffer = bytes(mod_out_silence) #txbuffer = bytes(mod_out_silence)
# TODO: Disabled this one for testing
txbuffer = bytes()
self.log.debug( self.log.debug(
"[MDM] TRANSMIT", mode=self.MODE, payload=payload_bytes_per_frame "[MDM] TRANSMIT", mode=self.MODE, payload=payload_bytes_per_frame
) )
@ -832,13 +851,21 @@ class RF:
) )
scatterdata = [] scatterdata = []
for i in range(codec2.MODEM_STATS_NC_MAX): # original function before itertool
for j in range(1, codec2.MODEM_STATS_NR_MAX, 2): #for i in range(codec2.MODEM_STATS_NC_MAX):
# print(f"{modemStats.rx_symbols[i][j]} - {modemStats.rx_symbols[i][j]}") # for j in range(1, codec2.MODEM_STATS_NR_MAX, 2):
xsymbols = round(modemStats.rx_symbols[i][j - 1] // 1000) # # print(f"{modemStats.rx_symbols[i][j]} - {modemStats.rx_symbols[i][j]}")
ysymbols = round(modemStats.rx_symbols[i][j] // 1000) # xsymbols = round(modemStats.rx_symbols[i][j - 1] // 1000)
if xsymbols != 0.0 and ysymbols != 0.0: # ysymbols = round(modemStats.rx_symbols[i][j] // 1000)
scatterdata.append({"x": str(xsymbols), "y": str(ysymbols)}) # if xsymbols != 0.0 and ysymbols != 0.0:
# scatterdata.append({"x": str(xsymbols), "y": str(ysymbols)})
for i, j in itertools.product(range(codec2.MODEM_STATS_NC_MAX), range(1, codec2.MODEM_STATS_NR_MAX, 2)):
# print(f"{modemStats.rx_symbols[i][j]} - {modemStats.rx_symbols[i][j]}")
xsymbols = round(modemStats.rx_symbols[i][j - 1] // 1000)
ysymbols = round(modemStats.rx_symbols[i][j] // 1000)
if xsymbols != 0.0 and ysymbols != 0.0:
scatterdata.append({"x": str(xsymbols), "y": str(ysymbols)})
# Send all the data if we have too-few samples, otherwise send a sampling # Send all the data if we have too-few samples, otherwise send a sampling
if 150 > len(scatterdata) > 0: if 150 > len(scatterdata) > 0:
@ -890,12 +917,11 @@ class RF:
- static.HAMLIB_BANDWIDTH - static.HAMLIB_BANDWIDTH
""" """
while True: while True:
threading.Event().wait(0.5) threading.Event().wait(0.25)
static.HAMLIB_FREQUENCY = self.hamlib.get_frequency() static.HAMLIB_FREQUENCY = self.hamlib.get_frequency()
static.HAMLIB_MODE = self.hamlib.get_mode() static.HAMLIB_MODE = self.hamlib.get_mode()
static.HAMLIB_BANDWIDTH = self.hamlib.get_bandwidth() static.HAMLIB_BANDWIDTH = self.hamlib.get_bandwidth()
static.HAMLIB_STATUS = self.hamlib.get_status() static.HAMLIB_STATUS = self.hamlib.get_status()
def calculate_fft(self) -> None: def calculate_fft(self) -> None:
""" """
Calculate an average signal strength of the channel to assess Calculate an average signal strength of the channel to assess
@ -945,8 +971,16 @@ class RF:
# calculate RMS and then dBFS # calculate RMS and then dBFS
# TODO: Need to change static.AUDIO_RMS to AUDIO_DBFS somewhen # TODO: Need to change static.AUDIO_RMS to AUDIO_DBFS somewhen
# https://dsp.stackexchange.com/questions/8785/how-to-compute-dbfs # https://dsp.stackexchange.com/questions/8785/how-to-compute-dbfs
rms = int(np.sqrt(np.max(d ** 2))) # try except for avoiding runtime errors by division/0
static.AUDIO_DBFS = 20 * np.log10(rms / 32768) try:
rms = int(np.sqrt(np.max(d ** 2)))
static.AUDIO_DBFS = 20 * np.log10(rms / 32768)
except Exception as e:
self.log.warning(
"[MDM] fft calculation error - please check your audio setup",
e=e,
)
static.AUDIO_DBFS = -100
rms_counter = 0 rms_counter = 0
@ -968,11 +1002,7 @@ class RF:
# 3200Hz = 315 # 3200Hz = 315
# define the area, we are detecting busy state # define the area, we are detecting busy state
if static.LOW_BANDWIDTH_MODE: dfft = dfft[120:176] if static.LOW_BANDWIDTH_MODE else dfft[65:231]
dfft = dfft[120:176]
else:
dfft = dfft[65:231]
# Check for signals higher than average by checking for "100" # Check for signals higher than average by checking for "100"
# If we have a signal, increment our channel_busy delay counter # If we have a signal, increment our channel_busy delay counter

View file

@ -4,6 +4,7 @@
# #
# modified and adjusted to FreeDATA needs by DJ2LS # modified and adjusted to FreeDATA needs by DJ2LS
import contextlib
import socket import socket
import time import time
import structlog import structlog
@ -101,7 +102,7 @@ class radio:
self.sock.close() self.sock.close()
self.connected = False self.connected = False
def send_command(self, command) -> bytes: def send_command(self, command, expect_answer) -> bytes:
"""Send a command to the connected rotctld instance, """Send a command to the connected rotctld instance,
and return the return value. and return the return value.
@ -122,7 +123,9 @@ class radio:
self.connected = False self.connected = False
try: try:
return self.connection.recv(1024) # recv seems to be blocking so in case of ptt we dont need the response
# maybe this speeds things up and avoids blocking states
return self.connection.recv(16) if expect_answer else True
except Exception: except Exception:
self.log.warning( self.log.warning(
"[RIGCTLD] No command response!", "[RIGCTLD] No command response!",
@ -146,11 +149,14 @@ class radio:
def get_mode(self): def get_mode(self):
""" """ """ """
try: try:
data = self.send_command(b"m") data = self.send_command(b"m", True)
data = data.split(b"\n") data = data.split(b"\n")
data = data[0].decode("utf-8") data = data[0].decode("utf-8")
if 'RPRT' not in data: if 'RPRT' not in data:
self.mode = data try:
data = int(data)
except ValueError:
self.mode = str(data)
return self.mode return self.mode
except Exception: except Exception:
@ -159,12 +165,13 @@ class radio:
def get_bandwidth(self): def get_bandwidth(self):
""" """ """ """
try: try:
data = self.send_command(b"m") data = self.send_command(b"m", True)
data = data.split(b"\n") data = data.split(b"\n")
data = data[1].decode("utf-8") data = data[1].decode("utf-8")
if 'RPRT' not in data: if 'RPRT' not in data and data not in ['']:
self.bandwidth = int(data) with contextlib.suppress(ValueError):
self.bandwidth = int(data)
return self.bandwidth return self.bandwidth
except Exception: except Exception:
return self.bandwidth return self.bandwidth
@ -172,11 +179,14 @@ class radio:
def get_frequency(self): def get_frequency(self):
""" """ """ """
try: try:
data = self.send_command(b"f") data = self.send_command(b"f", True)
data = data.decode("utf-8") data = data.decode("utf-8")
if 'RPRT' not in data: if 'RPRT' not in data and data not in [0, '0', '']:
self.frequency = data with contextlib.suppress(ValueError):
data = int(data)
# make sure we have a frequency and not bandwidth
if data >= 10000:
self.frequency = data
return self.frequency return self.frequency
except Exception: except Exception:
return self.frequency return self.frequency
@ -184,7 +194,7 @@ class radio:
def get_ptt(self): def get_ptt(self):
""" """ """ """
try: try:
return self.send_command(b"t") return self.send_command(b"t", True)
except Exception: except Exception:
return False return False
@ -199,9 +209,9 @@ class radio:
""" """
try: try:
if state: if state:
self.send_command(b"T 1") self.send_command(b"T 1", False)
else: else:
self.send_command(b"T 0") self.send_command(b"T 0", False)
return state return state
except Exception: except Exception:
return False return False

View file

@ -24,6 +24,7 @@ import socketserver
import sys import sys
import threading import threading
import time import time
import wave
import helpers import helpers
import static import static
@ -227,6 +228,28 @@ def process_tnc_commands(data):
"[SCK] CQ command execution error", e=err, command=received_json "[SCK] CQ command execution error", e=err, command=received_json
) )
# START STOP AUDIO RECORDING -----------------------------------------------------
if received_json["type"] == "set" and received_json["command"] == "record_audio":
try:
if not static.AUDIO_RECORD:
static.AUDIO_RECORD_FILE = wave.open(f"{int(time.time())}_audio_recording.wav", 'w')
static.AUDIO_RECORD_FILE.setnchannels(1)
static.AUDIO_RECORD_FILE.setsampwidth(2)
static.AUDIO_RECORD_FILE.setframerate(8000)
static.AUDIO_RECORD = True
else:
static.AUDIO_RECORD = False
static.AUDIO_RECORD_FILE.close()
command_response("respond_to_call", True)
except Exception as err:
command_response("respond_to_call", False)
log.warning(
"[SCK] CQ command execution error", e=err, command=received_json
)
# SET ENABLE/DISABLE RESPOND TO CALL ----------------------------------------------------- # SET ENABLE/DISABLE RESPOND TO CALL -----------------------------------------------------
if received_json["type"] == "set" and received_json["command"] == "respond_to_call": if received_json["type"] == "set" and received_json["command"] == "respond_to_call":
try: try:
@ -612,6 +635,7 @@ def send_tnc_state():
"dxgrid": str(static.DXGRID, encoding), "dxgrid": str(static.DXGRID, encoding),
"hamlib_status": static.HAMLIB_STATUS, "hamlib_status": static.HAMLIB_STATUS,
"listen": str(static.LISTEN), "listen": str(static.LISTEN),
"audio_recording": str(static.AUDIO_RECORD),
} }
# add heard stations to heard stations object # add heard stations to heard stations object
@ -864,3 +888,4 @@ def command_response(command, status):
jsondata = {"command_response": command, "status": s_status} jsondata = {"command_response": command, "status": s_status}
data_out = json.dumps(jsondata) data_out = json.dumps(jsondata)
SOCKET_QUEUE.put(data_out) SOCKET_QUEUE.put(data_out)

View file

@ -11,7 +11,7 @@ Not nice, suggestions are appreciated :-)
import subprocess import subprocess
from enum import Enum from enum import Enum
VERSION = "0.6.8-alpha.1" VERSION = "0.6.9-alpha.1-exp"
ENABLE_EXPLORER = False ENABLE_EXPLORER = False
@ -82,6 +82,8 @@ AUDIO_INPUT_DEVICES: list = []
AUDIO_OUTPUT_DEVICES: list = [] AUDIO_OUTPUT_DEVICES: list = []
AUDIO_INPUT_DEVICE: int = -2 AUDIO_INPUT_DEVICE: int = -2
AUDIO_OUTPUT_DEVICE: int = -2 AUDIO_OUTPUT_DEVICE: int = -2
AUDIO_RECORD: bool = False
AUDIO_RECORD_FILE = ''
BUFFER_OVERFLOW_COUNTER: list = [0, 0, 0, 0, 0] BUFFER_OVERFLOW_COUNTER: list = [0, 0, 0, 0, 0]
AUDIO_DBFS: int = 0 AUDIO_DBFS: int = 0
@ -90,7 +92,7 @@ ENABLE_FFT: bool = True
CHANNEL_BUSY: bool = False CHANNEL_BUSY: bool = False
# ARQ PROTOCOL VERSION # ARQ PROTOCOL VERSION
ARQ_PROTOCOL_VERSION: int = 4 ARQ_PROTOCOL_VERSION: int = 5
# ARQ statistics # ARQ statistics
ARQ_BYTES_PER_MINUTE_BURST: int = 0 ARQ_BYTES_PER_MINUTE_BURST: int = 0
@ -101,6 +103,8 @@ ARQ_COMPRESSION_FACTOR: int = 0
ARQ_TRANSMISSION_PERCENT: int = 0 ARQ_TRANSMISSION_PERCENT: int = 0
ARQ_SPEED_LEVEL: int = 0 ARQ_SPEED_LEVEL: int = 0
TOTAL_BYTES: int = 0 TOTAL_BYTES: int = 0
# set save to folder state for allowing downloading files to local file system
ARQ_SAVE_TO_FOLDER: bool = False
# CHANNEL_STATE = 'RECEIVING_SIGNALLING' # CHANNEL_STATE = 'RECEIVING_SIGNALLING'
TNC_STATE: str = "IDLE" TNC_STATE: str = "IDLE"

View file

@ -35,6 +35,10 @@ ip, port = args.socket_host, args.socket_port
connected = True connected = True
data = bytes() data = bytes()
"""
Nachricht
{'command': 'rx_buffer', 'data-array': [{'uuid': '8dde227d-3a09-4f39-b34c-5f8281d719d1', 'timestamp': 1672043316, 'dxcallsign': 'DJ2LS-1', 'dxgrid': 'JN48cs', 'data': 'bQA7c2VuZF9tZXNzYWdlADsxMjMAO2VkY2NjZDAyLTUzMTQtNDc3Ni1hMjlkLTFmY2M1ZDI4OTM4ZAA7VGVzdAoAOwA7cGxhaW4vdGV4dAA7ADsxNjcyMDQzMzA5'}]}
"""
def decode_and_save_data(encoded_data): def decode_and_save_data(encoded_data):
decoded_data = base64.b64decode(encoded_data) decoded_data = base64.b64decode(encoded_data)
@ -69,36 +73,42 @@ def decode_and_save_data(encoded_data):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((ip, port)) sock.connect((ip, port))
print(sock)
while connected: while connected:
chunk = sock.recv(2) chunk = sock.recv(1024)
data += chunk data += chunk
if data.startswith(b"{") and data.endswith(b"}\n"):
if data.startswith(b'{') and data.endswith(b'}\n'): # split data by \n if we have multiple commands in socket buffer
data = data.split(b"\n")
# remove empty data
data.remove(b"")
jsondata = json.loads(data.split(b'\n')[0]) # iterate through data list
data = bytes() for command in data:
if jsondata.get('command') == "tnc_state": jsondata = json.loads(command)
pass
if jsondata.get('freedata') == "tnc-message":
log.info(jsondata)
if jsondata.get('ping') == "acknowledge": if jsondata.get('command') == "tnc_state":
log.info(f"PING {jsondata.get('mycallsign')} >><< {jsondata.get('dxcallsign')}", snr=jsondata.get('snr'), dxsnr=jsondata.get('dxsnr')) pass
if jsondata.get('status') == 'receiving': if jsondata.get('freedata') == "tnc-message":
log.info(jsondata) log.info(jsondata)
if jsondata.get('command') == 'rx_buffer': if jsondata.get('ping') == "acknowledge":
for rxdata in jsondata["data-array"]: log.info(f"PING {jsondata.get('mycallsign')} >><< {jsondata.get('dxcallsign')}", snr=jsondata.get('snr'), dxsnr=jsondata.get('dxsnr'))
log.info(f"rx buffer {rxdata.get('uuid')}")
decode_and_save_data(rxdata.get('data'))
if jsondata.get('status') == 'received': if jsondata.get('status') == 'receiving':
decode_and_save_data(jsondata["data"]) log.info(jsondata)
if jsondata.get('command') == 'rx_buffer':
for rxdata in jsondata["data-array"]:
log.info(f"rx buffer {rxdata.get('uuid')}")
decode_and_save_data(rxdata.get('data'))
if jsondata.get('status') == 'received' and jsondata.get('arq') == 'transmission':
decode_and_save_data(jsondata["data"])
# clear data buffer as soon as data has been read # clear data buffer as soon as data has been read
data = bytes() data = bytes()

View file

@ -13,6 +13,7 @@ import base64
import json import json
import uuid import uuid
import time import time
import crcengine
# --------------------------------------------GET PARAMETER INPUTS # --------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description='Simons TEST TNC') parser = argparse.ArgumentParser(description='Simons TEST TNC')
@ -47,26 +48,32 @@ else:
# convert binary data to base64 # convert binary data to base64
#base64_data = base64.b64encode(file).decode("UTF-8") #base64_data = base64.b64encode(file).decode("UTF-8")
split_char = b'\0;' split_char = b'\0;\1;'
filetype = b"unknown" filetype = b"unknown"
timestamp = str(int(time.time())) timestamp = str(int(time.time()))
# timestamp = timestamp.to_bytes(4, byteorder="big") # timestamp = timestamp.to_bytes(4, byteorder="big")
timestamp = bytes(timestamp, "utf-8") timestamp = bytes(timestamp, "utf-8")
msg_with_attachment = chatmessage + \ msg_with_attachment = timestamp + \
split_char + \
chatmessage + \
split_char + \ split_char + \
filename + \ filename + \
split_char + \ split_char + \
filetype + \ filetype + \
split_char + \ split_char + \
file + \ file
split_char + \
timestamp # calculate checksum
crc_algorithm = crcengine.new("crc32") # load crc32 library
crc_data = crc_algorithm(file)
crc_data = crc_data.to_bytes(4, byteorder="big")
datatype = b"m" datatype = b"m"
command = b"send_message" command = b"send_message"
checksum = b"123" checksum = bytes(crc_data.hex(), "utf-8")
uuid_4 = bytes(str(uuid.uuid4()), "utf-8") uuid_4 = bytes(str(uuid.uuid4()), "utf-8")
data = datatype + \ data = datatype + \
@ -98,9 +105,13 @@ command = {"type": "arq",
] ]
} }
command = json.dumps(command) command = json.dumps(command)
print(command)
command = bytes(command + "\n", 'utf-8') command = bytes(command + "\n", 'utf-8')
# Create a socket (SOCK_STREAM means a TCP socket) # Create a socket (SOCK_STREAM means a TCP socket)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
# Connect to server and send data # Connect to server and send data
sock.connect((HOST, PORT)) sock.connect((HOST, PORT))
sock.sendall(command) sock.sendall(command)
timeout = time.time() + 5
while time.time() < timeout:
pass