Merge branch 'main' into update_github_action

This commit is contained in:
DJ2LS 2023-01-19 21:27:05 +01:00 committed by GitHub
commit 4c9ee09e86
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 4226 additions and 4546 deletions

View file

@ -323,8 +323,6 @@ jobs:
python3 -m nuitka --enable-plugin=numpy --assume-yes-for-downloads --standalone daemon.py
python3 -m nuitka --enable-plugin=numpy --assume-yes-for-downloads --standalone main.py
- name: Copy binaries - Linux
if: ${{startsWith(matrix.os, 'ubuntu')}}
working-directory: tnc
@ -350,6 +348,7 @@ jobs:
with:
path: tnc/dist/tnc
- name: LIST ALL FILES
run: ls -R
@ -371,6 +370,16 @@ jobs:
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
args: ${{ matrix.electron_parameters }}
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:
package_root: "./gui/"
github_token: ${{ secrets.github_token }}
# If the commit is tagged with a version (e.g. "v1.0.0"),
# release the app after building
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
args: ${{ matrix.electron_parameters }}
- name: Compress TNC
uses: thedoctor0/zip-release@master
with:
@ -405,4 +414,4 @@ jobs:
# with:
# name: app_bundle_${{ matrix.os }}.zip
# # path: ./tnc/dist/tnc/${{ matrix.zip_name }}.zip
# path: ./gui/dist/*
# path: ./gui/dist/*

View file

@ -9,16 +9,34 @@ jobs:
# cross-platform coverage.
# See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix
runs-on: ubuntu-latest
strategy:
# By default, GitHub will maximize the number of jobs run in parallel
# depending on the available runners on GitHub-hosted virtual machines.
# max-parallel: 8
fail-fast: false
matrix:
include:
- python-version: "3.7"
- python-version: "3.8"
- python-version: "3.9"
- python-version: "3.10"
- python-version: "3.11"
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install packages
shell: bash
run: |
sudo apt-get update
sudo apt-get install octave octave-common octave-signal sox python3 python3-pip portaudio19-dev python3-pyaudio
pip3 install psutil crcengine ujson pyserial numpy structlog sounddevice
sudo apt-get install octave octave-common octave-signal sox portaudio19-dev python3-pyaudio
pip3 install psutil crcengine ujson pyserial numpy structlog sounddevice pyaudio
pip3 install pytest pytest-rerunfailures
- name: Build codec2

View file

@ -50,12 +50,13 @@ add_test(NAME tnc_irs_iss
python3 test_tnc.py")
set_tests_properties(tnc_irs_iss PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
add_test(NAME chat_text
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_chat_text.py")
set_tests_properties(chat_text PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
# disabled this test as its actually broken since we entroduced session IDs
#add_test(NAME chat_text
# COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
# export PYTHONPATH=../tnc;
# cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
# python3 test_chat_text.py")
# set_tests_properties(chat_text PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
add_test(NAME datac0_frames
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;

View file

@ -21,9 +21,8 @@ Please keep in mind, this project is still under development with many issues wh
- [x] SNR operation level SNR > 0dB MPP/MPD
- [x] file compression
- [x] auto updater
- [ ] channel measurement
- [x] channel measurement
- [ ] hybrid ARQ
- [ ] SNR operation level SNR @ -5dB MPP/MPD
- [ ] tbc...
### existing/planned Chat features
- [x] chat messages

BIN
gui/build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 KiB

View file

@ -20,6 +20,10 @@ var socketchunk = ''; // Current message, per connection.
// global to keep track of daemon connection error emissions
var daemonShowConnectStateError = 1
// global for storing ip information
var daemon_port = config.daemon_port;
var daemon_host = config.daemon_host;
setTimeout(connectDAEMON, 500)
function connectDAEMON() {
@ -27,13 +31,13 @@ function connectDAEMON() {
daemonLog.info('connecting to daemon');
}
//clear message buffer after reconnecting or inital connection
//clear message buffer after reconnecting or initial connection
socketchunk = '';
if (config.tnclocation == 'localhost') {
daemon.connect(3001, '127.0.0.1')
} else {
daemon.connect(config.daemon_port, config.daemon_host)
daemon.connect(daemon_port, daemon_host)
}
@ -217,7 +221,7 @@ exports.getDaemonState = function() {
// START TNC
// ` `== multi line string
exports.startTNC = function(mycall, mygrid, rx_audio, tx_audio, radiocontrol, devicename, deviceport, pttprotocol, pttport, serialspeed, data_bits, stop_bits, handshake, rigctld_ip, rigctld_port, enable_fft, enable_scatter, low_bandwidth_mode, tuning_range_fmin, tuning_range_fmax, enable_fsk, tx_audio_level, respond_to_cq, rx_buffer_size) {
exports.startTNC = function(mycall, mygrid, rx_audio, tx_audio, radiocontrol, devicename, deviceport, pttprotocol, pttport, serialspeed, data_bits, stop_bits, handshake, rigctld_ip, rigctld_port, enable_fft, enable_scatter, low_bandwidth_mode, tuning_range_fmin, tuning_range_fmax, enable_fsk, tx_audio_level, respond_to_cq, rx_buffer_size, enable_explorer) {
var json_command = JSON.stringify({
type: 'set',
command: 'start_tnc',
@ -245,7 +249,8 @@ exports.startTNC = function(mycall, mygrid, rx_audio, tx_audio, radiocontrol, de
tuning_range_fmax : tuning_range_fmax,
tx_audio_level : tx_audio_level,
respond_to_cq : respond_to_cq,
rx_buffer_size : rx_buffer_size
rx_buffer_size : rx_buffer_size,
enable_explorer : enable_explorer
}]
})
@ -298,4 +303,19 @@ exports.saveMyGrid = function(grid) {
writeDaemonCommand(command)
}
ipcRenderer.on('action-update-daemon-ip', (event, arg) => {
daemon.destroy();
let Data = {
busy_state: "-",
arq_state: "-",
//channel_state: "-",
frequency: "-",
mode: "-",
bandwidth: "-",
dbfs_level: 0
};
ipcRenderer.send('request-update-tnc-state', Data);
daemon_port = arg.port;
daemon_host = arg.adress;
connectDAEMON();
});

View file

@ -10,12 +10,13 @@ const path = require('path');
const fs = require('fs');
const os = require('os');
const spawn = require('child_process').spawn;
const exec = require('child_process').exec;
const log = require('electron-log');
const mainLog = log.scope('main');
const daemonProcessLog = log.scope('freedata-daemon');
const mime = require('mime');
const net = require('net');
const sysInfo = log.scope('system information');
sysInfo.info("SYSTEM INFORMATION ----------------------------- ");
@ -90,7 +91,9 @@ const configDefaultSettings = '{\
"tuning_range_fmin" : "-50.0",\
"tuning_range_fmax" : "50.0",\
"respond_to_cq" : "True",\
"rx_buffer_size" : "16" \
"rx_buffer_size" : "16", \
"enable_explorer" : "False", \
"wftheme": 2 \
}';
if (!fs.existsSync(configPath)) {
@ -167,14 +170,31 @@ let data = null;
let logViewer = null;
var daemonProcess = null;
// create a splash screen
function createSplashScreen(){
splashScreen = new BrowserWindow({
height: 250,
width: 250,
transparent: true,
frame: false,
alwaysOnTop: true
});
splashScreen.loadFile('src/splash.html');
splashScreen.center();
}
function createWindow() {
win = new BrowserWindow({
width: config.screen_width,
height: config.screen_height,
show: false,
autoHideMenuBar: true,
icon: 'src/img/icon.png',
webPreferences: {
//preload: path.join(__dirname, 'preload-main.js'),
backgroundThrottle: false,
preload: require.resolve('./preload-main.js'),
nodeIntegration: true,
contextIsolation: false,
@ -283,8 +303,19 @@ function createWindow() {
}
app.whenReady().then(() => {
// show splash screen
createSplashScreen();
// create main window
createWindow();
// wait some time, then close splash screen and show main windows
setTimeout(function() {
splashScreen.close();
win.show();
}, 3000);
// start daemon by checking os
mainLog.info('Starting freedata-daemon binary');
@ -395,6 +426,16 @@ ipcMain.on('request-show-chat-window', () => {
chat.show();
});
// UPDATE TNC CONNECTION
ipcMain.on('request-update-tnc-ip',(event,data)=>{
win.webContents.send('action-update-tnc-ip', data);
});
// UPDATE DAEMON CONNECTION
ipcMain.on('request-update-daemon-ip',(event,data)=>{
win.webContents.send('action-update-daemon-ip', data);
});
ipcMain.on('request-update-tnc-state', (event, arg) => {
win.webContents.send('action-update-tnc-state', arg);
@ -519,7 +560,7 @@ ipcMain.on('save-file-to-folder',(event,data)=>{
console.log(data.file)
try {
let buffer = Buffer.from(data.file);
let arraybuffer = Uint8Array.from(buffer);
console.log(arraybuffer)
@ -540,6 +581,7 @@ ipcMain.on('save-file-to-folder',(event,data)=>{
});
//tnc messages START --------------------------------------
// CQ TRANSMITTING
@ -592,6 +634,12 @@ ipcMain.on('request-show-arq-toast-datachannel-opening',(event,data)=>{
win.webContents.send('action-show-arq-toast-datachannel-opening', data);
});
// ARQ DATA CHANNEL WAITING
ipcMain.on('request-show-arq-toast-datachannel-waiting',(event,data)=>{
win.webContents.send('action-show-arq-toast-datachannel-waiting', data);
});
// ARQ DATA CHANNEL OPEN
ipcMain.on('request-show-arq-toast-datachannel-opened',(event,data)=>{
win.webContents.send('action-show-arq-toast-datachannel-opened', data);
@ -632,6 +680,11 @@ ipcMain.on('request-show-arq-toast-session-connecting',(event,data)=>{
win.webContents.send('action-show-arq-toast-session-connecting', data);
});
// ARQ SESSION WAITING
ipcMain.on('request-show-arq-toast-session-waiting',(event,data)=>{
win.webContents.send('action-show-arq-toast-session-waiting', data);
});
// ARQ SESSION CONNECTED
ipcMain.on('request-show-arq-toast-session-connected',(event,data)=>{
win.webContents.send('action-show-arq-toast-session-connected', data);
@ -781,15 +834,24 @@ function close_all() {
// RUN RIGCTLD
ipcMain.on('request-start-rigctld',(event, data)=>{
try{
spawn(data.path, data.parameters);
let rigctld_proc = spawn(data.path, data.parameters);
rigctld_proc.on('exit', function (code) {
console.log('rigctld process exited with code ' + code);
// if rigctld crashes, error code is -2
// then we are going to restart rigctld
// this "fixes" a problem with latest rigctld on raspberry pi
//if (code == -2){
// setTimeout(ipcRenderer.send('request-start-rigctld', data), 500);
//}
//let rigctld_proc = spawn(data.path, data.parameters);
});
} catch (e) {
console.log(e);
}
/*
const rigctld = exec(data.path, data.parameters);
rigctld.stdout.on("data", data => {
@ -828,53 +890,60 @@ ipcMain.on('request-stop-rigctld',(event,data)=>{
// CHECK RIGCTLD
ipcMain.on('request-check-rigctld',(data)=>{
try {
// CHECK RIGCTLD CONNECTION
// create new socket so we are not reopening every time a new one
var rigctld_connection = new net.Socket();
var rigctld_connection_state = false;
ipcMain.on('request-check-rigctld',(event, data)=>{
try{
let Data = {
state: "unknown",
state: "unknown",
};
isRunning('rigctld', (status) => {
if (status){
Data["state"] = "running";
} else {
Data["state"] = "unknown/stopped";
if(!rigctld_connection_state){
rigctld_connection = new net.Socket();
rigctld_connection.connect(data.port, data.ip)
}
// check if we have created a new socket object
if (typeof(rigctld_connection) != 'undefined') {
rigctld_connection.on('connect', function() {
rigctld_connection_state = true;
Data["state"] = "connection possible - (" + data.ip + ":" + data.port + ")";
if (win !== null && win !== '' && typeof(win) != 'undefined'){
// try catch for being sure we have a clean app close
try{
win.webContents.send('action-check-rigctld', Data);
} catch(e){
console.log(e)
}
}
win.webContents.send('action-check-rigctld', Data);
})
} catch (e) {
mainLog.error(e)
rigctld_connection.on('error', function() {
rigctld_connection_state = false;
Data["state"] = "unknown/stopped - (" + data.ip + ":" + data.port + ")";
if (win !== null && win !== '' && typeof(win) != 'undefined'){
// try catch for being sure we have a clean app close
try{
win.webContents.send('action-check-rigctld', Data);
} catch(e){
console.log(e)
}
}
})
rigctld_connection.on('end', function() {
rigctld_connection_state = false;
})
}
} catch(e) {
console.log(e)
}
});
// https://stackoverflow.com/a/51084163
// Function for checking if a process is running or not
/*
isRunning('rigctld', (status) => {
if (status){
Data["state"] = "running";
} else {
Data["state"] = "unknown";
}
win.webContents.send('action-check-rigctld', Data);
})
*/
const isRunning = (query, cb) => {
let platform = process.platform;
let cmd = '';
switch (platform) {
case 'win32' : cmd = `tasklist`; break;
case 'darwin' : cmd = `ps -ax | grep ${query}`; break;
case 'linux' : cmd = `ps -A`; break;
default: break;
}
exec(cmd, (err, stdout) => {
cb(stdout.toLowerCase().indexOf(query.toLowerCase()) > -1);
});
}

View file

@ -1,6 +1,6 @@
{
"name": "FreeDATA",
"version": "0.5.0-alpha.1",
"version": "0.6.11-alpha.2",
"description": "FreeDATA ",
"main": "main.js",
"scripts": {
@ -32,16 +32,19 @@
"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",
"emoji-picker-element-data": "^1.3.0",
"express-pouchdb": "^4.2.0",
"mime": "^3.0.0",
"pouchdb": "^7.3.0",
"pouchdb-browser": "^7.3.0",
"pouchdb-express-router": "^0.0.11",
"pouchdb-find": "^7.3.0",
"pouchdb-replication": "^8.0.0",
"qth-locator": "^2.1.0",
"utf8": "^3.0.0",
"uuid": "^9.0.0"
@ -55,11 +58,11 @@
"appId": "app.freedata",
"npmRebuild": "false",
"directories": {
"buildResources": "src/img",
"buildResources": "build",
"output": "dist"
},
"dmg": {
"icon": "src/img/icon.png",
"icon": "build/icon.png",
"contents": [
{
"x": 130,
@ -74,13 +77,13 @@
]
},
"win": {
"icon": "src/img/icon.png",
"icon": "build/icon.png",
"target": [
"nsis"
]
},
"linux": {
"icon": "src/img/icon.png",
"icon": "build/icon.png",
"target": [
"AppImage"
],
@ -91,7 +94,13 @@
"releaseType": "release"
},
"extraResources": [
"./tnc/**"
{
"from": "../tnc/dist/tnc/",
"to": "tnc",
"filter": [
"**/*"
]
}
]
}
}

View file

@ -34,7 +34,7 @@ const dateFormatHours = new Intl.DateTimeFormat('en-GB', {
hour12: false,
});
// split character
const split_char = '\0;'
const split_char = '\0;\1;'
// global for our selected file we want to transmit
// ----------------- some chat globals
var filetype = '';
@ -61,7 +61,52 @@ try{
}
PouchDB.plugin(require('pouchdb-find'));
//PouchDB.plugin(require('pouchdb-replication'));
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,
retry: false
}).on('change', function (change) {
// yo, something changed!
console.log(change)
}).on('paused', function (err) {
// replication was paused, usually because of a lost connection
console.log(err)
}).on('active', function (info) {
// replication was resumed
console.log(info)
}).on('error', function (err) {
// totally unhandled error (shouldn't happen)
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();
db.createIndex({
index: {
@ -73,6 +118,7 @@ db.createIndex({
}).catch(function(err) {
console.log(err);
});
db.find({
selector: {
timestamp: {
@ -87,7 +133,15 @@ db.find({
if (typeof(result) !== 'undefined') {
result.docs.forEach(function(item) {
//console.log(item)
update_chat(item);
// another query with attachments
db.get(item._id, {
attachments: true
}).then(function(item_with_attachments){
update_chat(item_with_attachments);
});
});
}
}).catch(function(err) {
@ -174,18 +228,49 @@ window.addEventListener('DOMContentLoaded', () => {
document.getElementById("chatModuleMessage").addEventListener("input", () => {
var textarea = document.getElementById("chatModuleMessage");
var text = textarea.value;
if(document.getElementById("expand_textarea").checked){
var lines = 6
} else {
var lines = text.split("\n").length
if (lines >= 10){
lines = 10;
if (lines >= 6){
lines = 6;
}
var message_container_height_offset = 90 + (23*lines);
}
var message_container_height_offset = 130 + (20*lines);
var message_container_height = `calc(100% - ${message_container_height_offset}px)`;
document.getElementById("message-container").style.height = message_container_height;
textarea.rows = lines;
console.log(textarea.value)
})
document.getElementById("expand_textarea").addEventListener("click", () => {
var textarea = document.getElementById("chatModuleMessage");
if(document.getElementById("expand_textarea").checked){
var lines=6
document.getElementById("expand_textarea_button").className = "bi bi-chevron-compact-down";
} else {
var lines=1
document.getElementById("expand_textarea_button").className = "bi bi-chevron-compact-up";
}
var message_container_height_offset = 130 + (20*lines);
//var message_container_height_offset = 90 + (23*lines);
var message_container_height = `calc(100% - ${message_container_height_offset}px)`;
document.getElementById("message-container").style.height = message_container_height;
textarea.rows = lines;
console.log(textarea.rows)
})
// NEW CHAT
document.getElementById("createNewChatButton").addEventListener("click", () => {
@ -223,10 +308,12 @@ db.post({
var chatmessage = textarea.value;
// reset textarea size
var message_container_height_offset = 110;
var message_container_height_offset = 150;
var message_container_height = `calc(100% - ${message_container_height_offset}px)`;
document.getElementById("message-container").style.height = message_container_height;
textarea.rows = 1
document.getElementById("expand_textarea_button").className = "bi bi-chevron-compact-up";
document.getElementById("expand_textarea").checked = false;
console.log(file);
console.log(filename);
@ -234,9 +321,12 @@ db.post({
if (filetype == ''){
filetype = 'plain/text'
}
var data_with_attachment = chatmessage + split_char + filename + split_char + filetype + split_char + file;
var timestamp = Math.floor(Date.now() / 1000);
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 = ``;
var uuid = uuidv4();
console.log(data_with_attachment)
@ -246,17 +336,17 @@ db.post({
mode: 255,
frames: 1,
data: data_with_attachment,
checksum: '123',
checksum: file_checksum,
uuid: uuid
};
ipcRenderer.send('run-tnc-command', Data);
db.post({
_id: uuid,
timestamp: Math.floor(Date.now() / 1000),
timestamp: timestamp,
dxcallsign: dxcallsign,
dxgrid: 'null',
msg: chatmessage,
checksum: 'null',
checksum: file_checksum,
type: "transmit",
status: 'transmit',
uuid: uuid,
@ -342,7 +432,7 @@ ipcRenderer.on('action-new-msg-received', (event, arg) => {
//handle ping
if (item.ping == 'received') {
obj.timestamp = item.timestamp;
obj.timestamp = parseInt(item.timestamp);
obj.dxcallsign = item.dxcallsign;
obj.dxgrid = item.dxgrid;
obj.uuid = item.uuid;
@ -359,12 +449,9 @@ ipcRenderer.on('action-new-msg-received', (event, arg) => {
add_obj_to_database(obj)
update_chat_obj_by_uuid(obj.uuid);
// handle beacon
} else if (item.beacon == 'received') {
obj.timestamp = item.timestamp;
obj.timestamp = parseInt(item.timestamp);
obj.dxcallsign = item.dxcallsign;
obj.dxgrid = item.dxgrid;
obj.uuid = item.uuid;
@ -386,20 +473,23 @@ ipcRenderer.on('action-new-msg-received', (event, arg) => {
} else if (item.arq == 'transmission' && item.status == 'received') {
var encoded_data = atob(item.data);
var splitted_data = encoded_data.split(split_char);
obj.timestamp = item.timestamp;
console.log(splitted_data)
obj.timestamp = parseInt(splitted_data[4]);
obj.dxcallsign = item.dxcallsign;
obj.dxgrid = item.dxgrid;
obj.command = splitted_data[1];
obj.checksum = splitted_data[2];
// convert message to unicode from utf8 because of emojis
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.snr = 'null';
obj.type = 'received';
obj.filename = utf8.decode(splitted_data[5]);
obj.filetype = utf8.decode(splitted_data[6]);
obj.file = btoa(utf8.decode(splitted_data[7]));
obj.filename = utf8.decode(splitted_data[6]);
obj.filetype = utf8.decode(splitted_data[7]);
obj.file = btoa(splitted_data[8]);
add_obj_to_database(obj);
update_chat_obj_by_uuid(obj.uuid);
@ -437,17 +527,42 @@ update_chat = function(obj) {
var filename = Object.keys(obj._attachments)[0]
var filetype = filename.split('.')[1]
var filesize = obj._attachments[filename]["length"] + " Bytes";
if (filesize == 'undefined Bytes'){
// get filesize of new submitted data
// not that nice....
// we really should avoid converting back from base64 for performance reasons...
var filesize = Math.ceil(atob(obj._attachments[filename]["data"]).length) + "Bytes";
}
// check if image, then display it
if(filetype == 'image/png' || filetype =="png"){
var fileheader = `
<div class="card-header border-0 bg-transparent text-end p-0 mb-0 hover-overlay">
<img class="w-100 rounded-2" src="data:image/png;base64,${obj._attachments[filename]["data"]}">
<p class="text-right mb-0 p-1 text-black" style="text-align: right; font-size : 1rem">
<span class="p-1" style="text-align: right; font-size : 0.8rem">${filename}</span>
<span class="p-1" style="text-align: right; font-size : 0.8rem">${filesize}</span>
<i class="bi bi-filetype-${filetype}" style="font-size: 2rem;"></i>
</p>
</div>
<hr class="m-0 p-0">
`;
}else{
var fileheader = `
<div class="card-header border-0 bg-transparent text-end p-0 mb-0 hover-overlay">
<p class="text-right mb-0 p-1 text-black" style="text-align: right; font-size : 1rem">
<span class="p-1" style="text-align: right; font-size : 0.8rem">${filename}</span>
<span class="p-1" style="text-align: right; font-size : 0.8rem">${filesize}</span>
<i class="bi bi-filetype-${filetype}" style="font-size: 2rem;"></i>
<i class="bi bi-filetype-${filetype}" style="font-size: 2rem;"></i>
</p>
</div>
<hr class="m-0 p-0">
`;
}
var controlarea_transmit = `
<div class="ms-auto" id="msg-${obj._id}-control-area">
@ -580,7 +695,7 @@ update_chat = function(obj) {
<div class="card border-light bg-light" id="msg-${obj._id}">
${fileheader}
<div class="card-body p-0">
<div class="card-body rounded-3 p-0">
<p class="card-text p-2 mb-0 text-break text-wrap">${message_html}</p>
<p class="text-right mb-0 p-1 text-white" style="text-align: left; font-size : 0.9rem">
<span class="badge bg-light text-muted">${timestamp}</span>
@ -621,7 +736,7 @@ update_chat = function(obj) {
<div class="card border-primary bg-primary" id="msg-${obj._id}">
${fileheader}
<div class="card-body p-0 text-right bg-primary">
<div class="card-body rounded-3 p-0 text-right bg-primary">
<p class="card-text p-1 mb-0 text-white text-break text-wrap">${message_html}</p>
<p class="text-right mb-0 p-1 text-white" style="text-align: right; font-size : 0.9rem">
<span class="text-light" style="font-size: 0.7rem;">${timestamp} - </span>
@ -675,6 +790,9 @@ update_chat = function(obj) {
if (obj.percent >= 100){
//document.getElementById('msg-' + obj._id + '-progress').classList.remove("progress-bar-striped");
document.getElementById('msg-' + obj._id + '-progress').classList.remove("progress-bar-animated");
document.getElementById('msg-' + obj._id + '-progress').classList.remove("bg-danger");
document.getElementById('msg-' + obj._id + '-progress').classList.add("bg-primary");
document.getElementById('msg-' + obj._id + '-progress').innerHTML = '';
} else {
document.getElementById('msg-' + obj._id + '-progress').classList.add("progress-bar-striped");
@ -734,28 +852,34 @@ update_chat = function(obj) {
//var file = atob(obj._attachments[filename]["data"])
db.getAttachment(obj._id, filename).then(function(data) {
console.log(data)
// convert blob data to binary string
blobUtil.blobToBinaryString(data).then(function (binaryString) {
console.log(binaryString)
}).catch(function (err) {
// error
console.log(err);
binaryString = blobUtil.arrayBufferToBinaryString(data);
}).then(function(){
var file = blobUtil.arrayBufferToBinaryString(data)
// converting back to blob for debugging
// length must be equal of file size
var blob = blobUtil.binaryStringToBlob(file);
console.log(blob)
var data_with_attachment = doc.msg + split_char + filename + split_char + filetype + split_char + file;
let Data = {
command: "send_message",
dxcallsign: doc.dxcallsign,
mode: 255,
frames: 1,
data: data_with_attachment,
checksum: doc.checksum,
uuid: doc.uuid
};
console.log(Data)
ipcRenderer.send('run-tnc-command', Data);
console.log(binaryString)
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 = {
command: "send_message",
dxcallsign: doc.dxcallsign,
mode: 255,
frames: 1,
data: data_with_attachment,
checksum: doc.checksum,
uuid: doc.uuid
};
console.log(Data)
ipcRenderer.send('run-tnc-command', Data);
});
});
}).catch(function(err) {
console.log(err);
@ -788,6 +912,7 @@ function saveFileToFolder(id) {
console.log(data.length)
//data = new Blob([data.buffer], { type: 'image/png' } /* (1) */)
console.log(data)
// we need to encode data because of error "an object could not be cloned"
let Data = {
file: data,
filename: filename,
@ -805,7 +930,7 @@ function saveFileToFolder(id) {
}
// function for setting an ICON to the correspinding state
// function for setting an ICON to the corresponding state
function get_icon_for_state(state) {
if (state == 'transmit') {
var status_icon = '<i class="bi bi-check" style="font-size:1rem;"></i>';
@ -841,7 +966,7 @@ update_chat_obj_by_uuid = function(uuid) {
add_obj_to_database = function(obj){
db.put({
_id: obj.uuid,
timestamp: obj.timestamp,
timestamp: parseInt(obj.timestamp),
uuid: obj.uuid,
dxcallsign: obj.dxcallsign,
dxgrid: obj.dxgrid,
@ -870,4 +995,37 @@ add_obj_to_database = function(obj){
function scrollMessagesToBottom() {
var messageBody = document.getElementById('message-container');
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

@ -8,14 +8,55 @@ var configPath = path.join(configFolder, 'config.json')
const config = require(configPath);
// WINDOW LISTENER
window.addEventListener('DOMContentLoaded', () => {
// here we could add filter buttons, somewhen later..
document.getElementById('enable_filter_info').addEventListener('click', () => {
if (document.getElementById('enable_filter_info').checked){
display_class("table-info", true)
} else {
display_class("table-info", false)
}
})
document.getElementById('enable_filter_debug').addEventListener('click', () => {
if (document.getElementById('enable_filter_debug').checked){
display_class("table-debug", true)
} else {
display_class("table-debug", false)
}
})
document.getElementById('enable_filter_warning').addEventListener('click', () => {
if (document.getElementById('enable_filter_warning').checked){
display_class("table-warning", true)
} else {
display_class("table-warning", false)
}
})
document.getElementById('enable_filter_error').addEventListener('click', () => {
if (document.getElementById('enable_filter_error').checked){
display_class("table-danger", true)
} else {
display_class("table-danger", false)
}
})
})
function display_class(class_name, state){
var collection = document.getElementsByClassName(class_name);
console.log(collection)
for (let i = 0; i < collection.length; i++) {
if (state == true){
collection[i].style.display = "table-row";
} else {
collection[i].style.display = "None";
}
}
}
ipcRenderer.on('action-update-log', (event, arg) => {
var entry = arg.entry
@ -24,46 +65,124 @@ ipcRenderer.on('action-update-log', (event, arg) => {
// https://stackoverflow.com/a/29497680
entry = entry.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,'')
var tbl = document.getElementById("log");
var row = document.createElement("tr");
var timestamp = document.createElement("td");
var timestampText = document.createElement('span');
datetime = new Date();
timestampText.innerText = datetime.toISOString();
//datetime = new Date();
//timestampText.innerText = datetime.toISOString();
timestampText.innerText = entry.slice(0, 19);
timestamp.appendChild(timestampText);
var type = document.createElement("td");
var typeText = document.createElement('span');
// typeText.innerText = entry.slice(10, 30).match(/[\[](.*)[^\]]/g);
console.log(entry.match(/\[[^\]]+\]/g))
try{
typeText.innerText = entry.match(/\[[^\]]+\]/g)[0];
} catch(e){
typeText.innerText = '-'
}
// let res = str.match(/[\[](.*)[^\]]/g);
type.appendChild(typeText);
var area = document.createElement("td");
var areaText = document.createElement('span');
//areaText.innerText = entry.slice(10, 50).match(/[\] \[](.*)[^\]]/g);
//areaText.innerText = entry.match(/\[[^\]]+\]/g)[1];
try{
areaText.innerText = entry.match(/\[[^\]]+\]/g)[1];
} catch(e){
areaText.innerText = '-'
}
area.appendChild(areaText);
var logEntry = document.createElement("td");
var logEntryText = document.createElement('span');
logEntryText.innerText = entry
try{logEntryText.innerText = entry.split("]")[2];
} catch(e){
logEntryText.innerText = "-";
}
logEntry.appendChild(logEntryText);
row.appendChild(timestamp);
row.appendChild(type);
row.appendChild(area);
row.appendChild(logEntry);
//row.classList.add("table-blablubb");
/*
if (logEntryText.innerText.includes('ALSA lib pcm')) {
row.classList.add("table-secondary");
}
*/
if (typeText.innerText.includes('info')) {
row.classList.add("table-info");
}
if (typeText.innerText.includes('debug')) {
row.classList.add("table-secondary");
}
if (typeText.innerText.includes('warning')) {
row.classList.add("table-warning");
}
if (typeText.innerText.includes('error')) {
row.classList.add("table-danger");
}
if (document.getElementById('enable_filter_info').checked) {
row.style.display = "table-row"
display_class("table-info", true)
} else {
row.style.display = "None"
display_class("table-info", false)
}
if (document.getElementById('enable_filter_debug').checked) {
row.style.display = "table-row"
display_class("table-secondary", true)
} else {
row.style.display = "None"
display_class("table-secondary", false)
}
if (document.getElementById('enable_filter_warning').checked) {
row.style.display = "table-row"
display_class("table-warning", true)
} else {
row.style.display = "None"
display_class("table-warning", false)
}
if (document.getElementById('enable_filter_error').checked) {
row.style.display = "table-row"
display_class("table-danger", true)
} else {
row.style.display = "None"
display_class("table-danger", false)
}
tbl.appendChild(row);
if (logEntryText.innerText.includes('ALSA lib pcm')) {
row.classList.add("table-secondary");
}
if (logEntryText.innerText.includes('[info ]')) {
row.classList.add("table-info");
}
if (logEntryText.innerText.includes('[debug ]')) {
row.classList.add("table-secondary");
}
if (logEntryText.innerText.includes('[warning ]')) {
row.classList.add("table-warning");
}
if (logEntryText.innerText.includes('[error ]')) {
row.classList.add("table-danger");
}
// scroll to bottom of page
// https://stackoverflow.com/a/11715670

View file

@ -1,5 +1,5 @@
const path = require('path');
const {ipcRenderer} = require('electron');
const {ipcRenderer, shell} = require('electron');
const exec = require('child_process').spawn;
const sock = require('./sock.js');
const daemon = require('./daemon.js');
@ -12,7 +12,7 @@ const {
} = require('qth-locator');
const os = require('os');
// split character used for appending addiotional data to files
// split character used for appending additional data to files
const split_char = '\0;';
@ -22,6 +22,14 @@ var configFolder = path.join(appDataFolder, "FreeDATA");
var configPath = path.join(configFolder, 'config.json');
const config = require(configPath);
// SET dbfs LEVEL GLOBAL
// this is an attempt of reducing CPU LOAD
// we are going to check if we have unequal values before we start calculating again
var dbfs_level_raw = 0
// START INTERVALL COMMAND EXECUTION FOR STATES
//setInterval(sock.getRxBuffer, 1000);
@ -29,6 +37,87 @@ const config = require(configPath);
// 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", () => {
let Data = {
type: "set",
command: "record_audio",
};
ipcRenderer.send('run-tnc-command', Data);
});
document.getElementById('received_files_folder').addEventListener('click', () => {
@ -88,8 +177,8 @@ document.getElementById('openReceivedFilesFolder').addEventListener('click', ()
// hamlib settings
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_ptt_port", "hamlib_ptt_port", config.enable_hamlib_ptt_port)
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)
document.getElementById('hamlib_serialspeed').value = config.hamlib_serialspeed;
set_setting_switch("enable_hamlib_serialspeed", "hamlib_serialspeed", config.enable_hamlib_serialspeed)
@ -160,7 +249,13 @@ set_setting_switch("enable_hamlib_ptt_port", "hamlib_ptt_port", config.enable_ha
document.getElementById("respondCQSwitch").checked = true;
} else {
document.getElementById("respondCQSwitch").checked = false;
}
}
if(config.enable_explorer == 'True'){
document.getElementById("ExplorerSwitch").checked = true;
} else {
document.getElementById("ExplorerSwitch").checked = false;
}
// theme selector
if(config.theme != 'default'){
@ -194,16 +289,57 @@ set_setting_switch("enable_hamlib_ptt_port", "hamlib_ptt_port", config.enable_ha
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
@ -277,10 +413,13 @@ set_setting_switch("enable_hamlib_ptt_port", "hamlib_ptt_port", config.enable_ha
// Create spectrum object on canvas with ID "waterfall"
global.spectrum = new Spectrum(
"waterfall", {
spectrumPercent: 0
spectrumPercent: 0,
wf_rows: 192 //Assuming 1 row = 1 pixe1, 192 is the height of the spectrum container
});
//Set waterfalltheme
document.getElementById("wftheme_selector").value = config.wftheme;
spectrum.setColorMap(config.wftheme);
// on click radio control toggle view
// disabled
@ -649,7 +788,7 @@ document.getElementById('hamlib_rigctld_start').addEventListener('click', () =>
document.getElementById('hamlib_rigctld_command').value = paramList
document.getElementById('hamlib_rigctld_command').value = paramList.join(" ") // join removes the commas
console.log(paramList)
console.log(rigctldPath)
@ -676,21 +815,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
@ -709,13 +882,7 @@ document.getElementById('hamlib_rigctld_stop').addEventListener('click', () => {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
});
// on change port and host
document.getElementById("tnc_adress").addEventListener("change", () => {
console.log(document.getElementById("tnc_adress").value);
config.tnc_host = document.getElementById("tnc_adress").value;
config.daemon_host = document.getElementById("tnc_adress").value;
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
});
// on change ping callsign
document.getElementById("dxCall").addEventListener("change", () => {
@ -727,13 +894,45 @@ document.getElementById('hamlib_rigctld_stop').addEventListener('click', () => {
});
// on change port and host
document.getElementById("tnc_adress").addEventListener("change", () => {
console.log(document.getElementById("tnc_adress").value);
config.tnc_host = document.getElementById("tnc_adress").value;
config.daemon_host = document.getElementById("tnc_adress").value;
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
let Data = {
port: document.getElementById("tnc_port").value,
adress: document.getElementById("tnc_adress").value,
};
ipcRenderer.send('request-update-tnc-ip', Data);
Data = {
port: parseInt(document.getElementById("tnc_port").value) + 1,
adress: document.getElementById("tnc_adress").value,
};
ipcRenderer.send('request-update-daemon-ip', Data);
});
// on change tnc port
document.getElementById("tnc_port").addEventListener("change", () => {
config.tnc_port = document.getElementById("tnc_port").value;
config.daemon_port = parseInt(document.getElementById("tnc_port").value) + 1;
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
let Data = {
port: document.getElementById("tnc_port").value,
adress: document.getElementById("tnc_adress").value,
};
ipcRenderer.send('request-update-tnc-ip', Data);
Data = {
port: parseInt(document.getElementById("tnc_port").value) + 1,
adress: document.getElementById("tnc_adress").value,
};
ipcRenderer.send('request-update-daemon-ip', Data);
});
// on change audio TX Level
@ -758,22 +957,21 @@ document.getElementById('hamlib_rigctld_stop').addEventListener('click', () => {
ipcRenderer.send('run-tnc-command', Data);
});
// saveMyCall button clicked
document.getElementById("saveMyCall").addEventListener("click", () => {
document.getElementById("myCall").addEventListener("input", () => {
callsign = document.getElementById("myCall").value;
ssid = document.getElementById("myCallSSID").value;
callsign_ssid = callsign.toUpperCase() + '-' + ssid;
config.mycall = callsign_ssid;
// split document title by looking for Call then split and update it
var documentTitle = document.title.split('Call:')
document.title = documentTitle[0] + 'Call: ' + callsign_ssid;
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
daemon.saveMyCall(callsign_ssid);
});
// saveMyGrid button clicked
document.getElementById("saveMyGrid").addEventListener("click", () => {
document.getElementById("myGrid").addEventListener("input", () => {
grid = document.getElementById("myGrid").value;
config.mygrid = grid;
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
@ -872,7 +1070,16 @@ document.getElementById('hamlib_rigctld_stop').addEventListener('click', () => {
}
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
});
// enable explorer Switch clicked
document.getElementById("ExplorerSwitch").addEventListener("click", () => {
if(document.getElementById("ExplorerSwitch").checked == true){
config.enable_explorer = "True";
} else {
config.enable_explorer = "False";
}
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
});
// enable fsk Switch clicked
document.getElementById("fskModeSwitch").addEventListener("click", () => {
@ -916,9 +1123,17 @@ document.getElementById('hamlib_rigctld_stop').addEventListener('click', () => {
config.theme = theme;
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
});
// Waterfall theme selector changed
document.getElementById("wftheme_selector").addEventListener("change", () => {
var wftheme = document.getElementById("wftheme_selector").value;
spectrum.setColorMap(wftheme);
config.wftheme = wftheme;
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
});
// Update channel selector clicked
// Update channel selector clicked
document.getElementById("update_channel_selector").addEventListener("click", () => {
config.update_channel = document.getElementById("update_channel_selector").value;
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
@ -947,6 +1162,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", () => {
@ -1008,6 +1228,11 @@ document.getElementById('hamlib_rigctld_stop').addEventListener('click', () => {
var respond_to_cq = "False";
}
if (document.getElementById("ExplorerSwitch").checked == true){
var enable_explorer = "True";
} else {
var enable_explorer = "False";
}
// loop through audio device list and select
for(i = 0; i < document.getElementById("audio_input_selectbox").length; i++) {
@ -1067,6 +1292,7 @@ document.getElementById('hamlib_rigctld_stop').addEventListener('click', () => {
config.tx_audio_level = tx_audio_level;
config.respond_to_cq = respond_to_cq;
config.rx_buffer_size = rx_buffer_size;
config.enable_explorer = enable_explorer;
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
@ -1085,7 +1311,7 @@ document.getElementById('hamlib_rigctld_stop').addEventListener('click', () => {
*/
daemon.startTNC(callsign_ssid, mygrid, rx_audio, tx_audio, radiocontrol, deviceid, deviceport, pttprotocol, pttport, serialspeed, data_bits, stop_bits, handshake, rigctld_ip, rigctld_port, enable_fft, enable_scatter, low_bandwidth_mode, tuning_range_fmin, tuning_range_fmax, enable_fsk, tx_audio_level, respond_to_cq, rx_buffer_size);
daemon.startTNC(callsign_ssid, mygrid, rx_audio, tx_audio, radiocontrol, deviceid, deviceport, pttprotocol, pttport, serialspeed, data_bits, stop_bits, handshake, rigctld_ip, rigctld_port, enable_fft, enable_scatter, low_bandwidth_mode, tuning_range_fmin, tuning_range_fmax, enable_fsk, tx_audio_level, respond_to_cq, rx_buffer_size, enable_explorer);
})
@ -1259,28 +1485,24 @@ ipcRenderer.on('action-update-tnc-state', (event, arg) => {
}
// 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
if (typeof(arg.mycallsign) !== 'undefined') {
// split document title by looking for Call then split and update it
var documentTitle = document.title.split('Call:')
document.title = documentTitle[0] + 'Call: ' + arg.mycallsign;
}
document.getElementById("toe").innerHTML = toe + ' ms'
*/
// update mygrid information with data from tnc
if (typeof(arg.mygrid) !== 'undefined') {
document.getElementById("myGrid").value = arg.mygrid;
}
// 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,
@ -1334,16 +1556,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)'
}],
};
@ -1353,25 +1571,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";
@ -1381,6 +1696,22 @@ ipcRenderer.on('action-update-tnc-state', (event, arg) => {
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
if (arg.channel_busy == 'True') {
document.getElementById("channel_busy").className = "btn btn-sm btn-danger";
@ -1438,6 +1769,14 @@ ipcRenderer.on('action-update-tnc-state', (event, arg) => {
}
// HAMLIB STATUS
if (arg.hamlib_status == 'connected') {
document.getElementById("rigctld_state").className = "btn btn-success btn-sm";
} else {
document.getElementById("rigctld_state").className = "btn btn-secondary btn-sm";
}
// BEACON STATE
@ -1457,15 +1796,22 @@ ipcRenderer.on('action-update-tnc-state', (event, arg) => {
document.getElementById("stopBeacon").disabled = true;
document.getElementById("beaconInterval").disabled = false;
}
// RMS
/*
var rms_level = Math.round((arg.rms_level/60) * 100)
document.getElementById("rms_level").setAttribute("aria-valuenow", rms_level);
document.getElementById("rms_level").setAttribute("style", "width:" + rms_level + "%;");
*/
// dbfs
// https://www.moellerstudios.org/converting-amplitude-representations/
if (dbfs_level_raw != arg.dbfs_level){
dbfs_level_raw = arg.dbfs_level
dbfs_level = Math.pow(10, arg.dbfs_level / 20) * 100
document.getElementById("dbfs_level_value").innerHTML = Math.round(arg.dbfs_level) + ' dBFS'
document.getElementById("dbfs_level").setAttribute("aria-valuenow", dbfs_level);
document.getElementById("dbfs_level").setAttribute("style", "width:" + dbfs_level + "%;");
}
// 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;
@ -1488,7 +1834,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
@ -1507,6 +1876,7 @@ ipcRenderer.on('action-update-tnc-state', (event, arg) => {
if(arg.speed_level >= 4) {
document.getElementById("speed_level").className = "bi bi-reception-4";
}
@ -2003,10 +2373,20 @@ ipcRenderer.on('run-tnc-command', (event, arg) => {
if (arg.command == 'set_tx_audio_level') {
sock.setTxAudioLevel(arg.tx_audio_level);
}
if (arg.command == 'record_audio') {
sock.record_audio();
}
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);
}
});
@ -2152,6 +2532,14 @@ ipcRenderer.on('action-show-arq-toast-datachannel-opening', (event, data) => {
toast.show();
});
// DATA CHANNEL WAITING TOAST
ipcRenderer.on('action-show-arq-toast-datachannel-waiting', (event, data) => {
var toastDATACHANNELwaiting = document.getElementById('toastDATACHANNELwaiting');
var toast = bootstrap.Toast.getOrCreateInstance(toastDATACHANNELwaiting); // Returns a Bootstrap toast instance
toast.show();
});
// DATA CHANNEL OPEN TOAST
ipcRenderer.on('action-show-arq-toast-datachannel-open', (event, data) => {
var toastDATACHANNELopen = document.getElementById('toastDATACHANNELopen');
@ -2204,10 +2592,43 @@ ipcRenderer.on('action-show-arq-toast-transmission-transmitted', (event, data) =
// ARQ TRANSMISSION TRANSMITTING
ipcRenderer.on('action-show-arq-toast-transmission-transmitting', (event, data) => {
document.getElementById("transmission_progress").className = "progress-bar progress-bar-striped progress-bar-animated bg-primary";
var toastARQtransmitting = document.getElementById('toastARQtransmitting');
var toast = bootstrap.Toast.getOrCreateInstance(toastARQtransmitting); // Returns a Bootstrap toast instance
toast.show();
//document.getElementById("toastARQtransmittingSNR").className = "progress-bar progress-bar-striped progress-bar-animated bg-primary";
var toastARQtransmittingSNR = document.getElementById('toastARQtransmittingSNR');
var toast = bootstrap.Toast.getOrCreateInstance(toastARQtransmittingSNR); // Returns a Bootstrap toast instance
var irs_snr = data["data"][0].irs_snr;
if(irs_snr <= 0){
document.getElementById("toastARQtransmittingSNR").className = "toast align-items-center text-white bg-danger border-0";
document.getElementById('toastARQtransmittingSNRValue').innerHTML = " low " + irs_snr;
toast.show();
} else if(irs_snr > 0 && irs_snr <= 5){
document.getElementById("toastARQtransmittingSNR").className = "toast align-items-center text-white bg-warning border-0";
document.getElementById('toastARQtransmittingSNRValue').innerHTML = " okay " + irs_snr;
toast.show();
} else if(irs_snr > 5 && irs_snr < 12.7){
document.getElementById("toastARQtransmittingSNR").className = "toast align-items-center text-white bg-success border-0";
document.getElementById('toastARQtransmittingSNRValue').innerHTML = " good " + irs_snr;
toast.show();
} else if(irs_snr >= 12.7){
document.getElementById("toastARQtransmittingSNR").className = "toast align-items-center text-white bg-success border-0";
document.getElementById('toastARQtransmittingSNRValue').innerHTML = " really good 12.7+";
toast.show();
} else {
console.log("no snr info available")
document.getElementById("transmission_progress").className = "progress-bar progress-bar-striped progress-bar-animated bg-primary";
var toastARQtransmitting = document.getElementById('toastARQtransmitting');
var toast = bootstrap.Toast.getOrCreateInstance(toastARQtransmitting); // Returns a Bootstrap toast instance
toast.show();
}
});
// ARQ TRANSMISSION RECEIVED
@ -2223,15 +2644,15 @@ ipcRenderer.on('action-show-arq-toast-transmission-received', (event, data) => {
ipcRenderer.on('action-show-arq-toast-transmission-receiving', (event, data) => {
document.getElementById("transmission_progress").className = "progress-bar progress-bar-striped progress-bar-animated bg-primary";
var toastARQreceiving = document.getElementById('toastARQreceiving');
var toast = bootstrap.Toast.getOrCreateInstance(toastARQreceiving); // Returns a Bootstrap toast instance
var toastARQsessionreceiving = document.getElementById('toastARQreceiving');
var toast = bootstrap.Toast.getOrCreateInstance(toastARQsessionreceiving); // Returns a Bootstrap toast instance
toast.show();
});
// ARQ SESSION CONNECTING
ipcRenderer.on('action-show-arq-toast-session-connecting', (event, data) => {
var toastARQreceiving = document.getElementById('toastARQsessionconnecting');
var toastARQsessionconnecting = document.getElementById('toastARQsessionconnecting');
var toast = bootstrap.Toast.getOrCreateInstance(toastARQsessionconnecting); // Returns a Bootstrap toast instance
toast.show();
});
@ -2239,15 +2660,24 @@ ipcRenderer.on('action-show-arq-toast-session-connecting', (event, data) => {
// ARQ SESSION CONNECTED
ipcRenderer.on('action-show-arq-toast-session-connected', (event, data) => {
var toastARQreceiving = document.getElementById('toastARQsessionconnected');
var toastARQsessionconnected = document.getElementById('toastARQsessionconnected');
var toast = bootstrap.Toast.getOrCreateInstance(toastARQsessionconnected); // Returns a Bootstrap toast instance
toast.show();
});
// ARQ SESSION CONNECTED
ipcRenderer.on('action-show-arq-toast-session-waiting', (event, data) => {
var toastARQsessionwaiting = document.getElementById('toastARQsessionwaiting');
var toast = bootstrap.Toast.getOrCreateInstance(toastARQsessionwaiting); // Returns a Bootstrap toast instance
toast.show();
});
// ARQ SESSION CLOSE
ipcRenderer.on('action-show-arq-toast-session-close', (event, data) => {
var toastARQreceiving = document.getElementById('toastARQsessionclose');
var toastARQsessionclose = document.getElementById('toastARQsessionclose');
var toast = bootstrap.Toast.getOrCreateInstance(toastARQsessionclose); // Returns a Bootstrap toast instance
toast.show();
});
@ -2255,7 +2685,7 @@ ipcRenderer.on('action-show-arq-toast-session-close', (event, data) => {
// ARQ SESSION FAILED
ipcRenderer.on('action-show-arq-toast-session-failed', (event, data) => {
var toastARQreceiving = document.getElementById('toastARQsessionfailed');
var toastARQsessionfailed = document.getElementById('toastARQsessionfailed');
var toast = bootstrap.Toast.getOrCreateInstance(toastARQsessionfailed); // Returns a Bootstrap toast instance
toast.show();
});
@ -2286,9 +2716,17 @@ function set_setting_switch(setting_switch, enable_object, state){
setInterval(checkRigctld, 500)
function checkRigctld(){
ipcRenderer.send('request-check-rigctld');
var rigctld_ip = document.getElementById("hamlib_rigctld_ip").value;
var rigctld_port = document.getElementById("hamlib_rigctld_port").value;
let Data = {
ip: rigctld_ip,
port: rigctld_port
};
ipcRenderer.send('request-check-rigctld', Data);
}
ipcRenderer.on('action-check-rigctld', (event, data) => {
console.log(data)
document.getElementById("hamlib_rigctld_status").value = data["state"];
});
});

View file

@ -19,7 +19,7 @@ var client = new net.Socket();
var socketchunk = ''; // Current message, per connection.
// split character
const split_char = '\0;'
const split_char = '\0;\1;'
// globals for getting new data only if available so we are saving bandwidth
var rxBufferLengthTnc = 0
@ -30,6 +30,10 @@ var rxMsgBufferLengthGui = 0
// global to keep track of TNC connection error emissions
var tncShowConnectStateError = 1
// global for storing ip information
var tnc_port = config.tnc_port;
var tnc_host = config.tnc_host;
// network connection Timeout
setTimeout(connectTNC, 2000)
@ -37,13 +41,13 @@ function connectTNC() {
//exports.connectTNC = function(){
//socketLog.info('connecting to TNC...')
//clear message buffer after reconnecting or inital connection
//clear message buffer after reconnecting or initial connection
socketchunk = '';
if (config.tnclocation == 'localhost') {
client.connect(3000, '127.0.0.1')
} else {
client.connect(config.tnc_port, config.tnc_host)
client.connect(tnc_port, tnc_host)
}
}
@ -56,7 +60,7 @@ client.on('connect', function(data) {
frequency: "-",
mode: "-",
bandwidth: "-",
rms_level: 0
dbfs_level: 0
};
ipcRenderer.send('request-update-tnc-state', Data);
@ -85,7 +89,7 @@ client.on('error', function(data) {
frequency: "-",
mode: "-",
bandwidth: "-",
rms_level: 0
dbfs_level: 0
};
ipcRenderer.send('request-update-tnc-state', Data);
@ -191,6 +195,8 @@ client.on('data', function(socketdata) {
rxMsgBufferLengthTnc = data['rx_msg_buffer_length']
let Data = {
mycallsign: data['mycallsign'],
mygrid: data['mygrid'],
ptt_state: data['ptt_state'],
busy_state: data['tnc_state'],
arq_state: data['arq_state'],
@ -200,7 +206,7 @@ client.on('data', function(socketdata) {
speed_level: data['speed_level'],
mode: data['mode'],
bandwidth: data['bandwidth'],
rms_level: data['audio_rms'],
dbfs_level: data['audio_dbfs'],
fft: data['fft'],
channel_busy: data['channel_busy'],
scatter: data['scatter'],
@ -216,11 +222,17 @@ 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'],
stations: data['stations'],
beacon_state: data['beacon_state'],
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);
@ -305,6 +317,10 @@ client.on('data', function(socketdata) {
} else if (data['status'] == 'connected') {
ipcRenderer.send('request-show-arq-toast-session-connected', {data: [data]});
// ARQ OPENING
} else if (data['status'] == 'waiting') {
ipcRenderer.send('request-show-arq-toast-session-waiting', {data: [data]});
// ARQ OPENING
} else if (data['status'] == 'close') {
ipcRenderer.send('request-show-arq-toast-session-close', {data: [data]});
@ -326,6 +342,10 @@ client.on('data', function(socketdata) {
} else if (data['status'] == 'opening') {
ipcRenderer.send('request-show-arq-toast-datachannel-opening', {data: [data]});
// ARQ WAITING
} else if (data['status'] == 'waiting') {
ipcRenderer.send('request-show-arq-toast-datachannel-waiting', {data: [data]});
// ARQ TRANSMISSION FAILED
} else if (data['status'] == 'failed') {
@ -501,22 +521,27 @@ exports.sendFile = function(dxcallsign, mode, frames, filename, filetype, data,
// Send Message
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
data = utf8.encode(data)
socketLog.info(data)
//data = utf8.encode(data)
//socketLog.info(data)
var datatype = "m"
data = datatype + split_char + command + split_char + checksum + split_char + uuid + split_char + data
socketLog.info(data)
socketLog.info(btoa(data))
//socketLog.info(data)
console.log(data)
console.log("CHECKSUM" + checksum)
//socketLog.info(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_raw", "uuid" : "'+ uuid +'", "parameter" : [{"dxcallsign" : "' + dxcallsign + '", "mode" : "' + mode + '", "n_frames" : "' + frames + '", "data" : "' + data + '"}]}'
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("-------------------------------------")
writeTncCommand(command)
@ -554,7 +579,7 @@ exports.stopBeacon = function() {
// OPEN ARQ SESSION
exports.connectARQ = function(dxcallsign) {
command = '{"type" : "arq", "command" : "connect", "dxcallsign": "'+ dxcallsign + '"}'
command = '{"type" : "arq", "command" : "connect", "dxcallsign": "'+ dxcallsign + '", "attempts": "15"}'
writeTncCommand(command)
}
@ -564,8 +589,53 @@ exports.disconnectARQ = function() {
writeTncCommand(command)
}
// SEND SINE
// SEND TEST FRAME
exports.sendTestFrame = function() {
command = '{"type" : "set", "command" : "send_test_frame"}'
writeTncCommand(command)
}
// RECORD AUDIO
exports.record_audio = function() {
command = '{"type" : "set", "command" : "record_audio"}'
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 = {
busy_state: "-",
arq_state: "-",
//channel_state: "-",
frequency: "-",
mode: "-",
bandwidth: "-",
dbfs_level: 0
};
ipcRenderer.send('request-update-tnc-state', Data);
tnc_port = arg.port;
tnc_host = arg.adress;
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

@ -54,11 +54,15 @@
</div>
<hr class="m-0">
<! ------messages area ---------------------------------------------------------------------->
<div class="container overflow-auto" id="message-container" style="height: calc(100% - 110px);">
<div class="container overflow-auto" id="message-container" style="height: calc(100% - 150px);">
<div class="tab-content" id="nav-tabContent"> </div>
<!--<div class="container position-absolute bottom-0">--></div>
<!-- </div>-->
<div class="container-fluid mt-2 p-0">
<input type="checkbox" id="expand_textarea" class="btn-check" autocomplete="off">
<label class="btn d-flex justify-content-center" id="expand_textarea_label" for="expand_textarea"><i id="expand_textarea_button" class="bi bi-chevron-compact-up"></i></label>
<div class="input-group bottom-0 w-100">
<!--<input class="form-control" maxlength="8" style="max-width: 6rem; text-transform:uppercase; display:none" id="chatModuleDxCall" placeholder="DX CALL"></input>-->
<!--<button class="btn btn-sm btn-primary me-2" id="emojipickerbutton" type="button">--><i id="emojipickerbutton" class="bi bi-emoji-smile m-1" style="font-size: 1.5rem; color: grey;"></i><!--</button>-->

View file

@ -44,7 +44,10 @@
<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!">
<!--
<button class="btn btn-sm btn-primary me-2" data-bs-toggle="offcanvas" data-bs-target="#receivedFilesSidebar" id="openReceivedFiles" type="button" > <strong>Files </strong>
<i class="bi bi-file-earmark-arrow-up-fill" style="font-size: 1rem; color: white;"></i>
<i class="bi bi-file-earmark-arrow-down-fill" style="font-size: 1rem; color: white;"></i>
@ -52,7 +55,8 @@
</span> <span data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="Send files through HF. This is currently under development!">
<button class="btn btn-sm btn-primary me-2" id="openDataModule" data-bs-toggle="offcanvas" data-bs-target="#transmitFileSidebar" type="button" style="display: None;"> <strong>TX File </strong>
<i class="bi bi-file-earmark-arrow-up-fill" style="font-size: 1rem; color: white;"></i>
</button>
</button>
-->
</span> <span data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="true" title="Settings and Info">
<button type="button" id="infoModalButton" data-bs-toggle="modal" data-bs-target="#infoModal" class="btn btn-sm btn-secondary"><strong>Settings </strong>
<i class="bi bi-sliders" style="font-size: 1rem; color: white;"></i>
@ -136,6 +140,15 @@
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
<!-- DATACHANNEL WAITING -->
<div class="toast align-items-center text-white bg-warning border-0" id="toastDATACHANNELwaiting" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">DATACHANNEL BUSY! Waiting...</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
<!-- STOPPING TRANSMISSION -->
<div class="toast align-items-center text-white bg-danger border-0" id="toastTRANSMISSIONstopped" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
@ -181,10 +194,19 @@
<!-- ARQ TRANSMITTING -->
<div class="toast align-items-center text-white bg-secondary border-0" id="toastARQtransmitting" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">TRANSMITTING FILE...</div>
<div class="toast-body">TRANSMITTING...</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
<!-- ARQ SNR INFORMATION -->
<div class="toast align-items-center text-white bg-secondary border-0" id="toastARQtransmittingSNR" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">TRANSMITTING - SNR IRS:<span id="toastARQtransmittingSNRValue"></span></div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
<!-- ARQ TRANSMITTING FAILED -->
<div class="toast align-items-center text-white bg-danger border-0" id="toastARQtransmittingfailed" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
@ -213,6 +235,13 @@
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
<!-- ARQ SESSION WAITING-->
<div class="toast align-items-center text-white bg-warning border-0" id="toastARQsessionwaiting" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">CHANNEL BUSY - Waiting...</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
<!-- ARQ SESSION CLOSE-->
<div class="toast align-items-center text-white bg-success border-0" id="toastARQsessionclose" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
@ -703,7 +732,6 @@
<option value="14">14</option>
<option value="15">15</option>
</select>
<button class="btn btn-sm btn-success" id="saveMyCall" type="button"> <i class="bi bi-check2" style="font-size: 1rem; color: white;"></i> </button>
</div>
</div>
<div class="col-md-auto">
@ -711,7 +739,6 @@
<i class="bi bi-house-fill" style="font-size: 1rem; color: black;"></i>
</span>
<input type="text" class="form-control mr-1" style="max-width: 6rem" placeholder="locator" id="myGrid" maxlength="6" aria-label="Input group" aria-describedby="btnGroupAddon">
<button class="btn btn-sm btn-success" id="saveMyGrid" type="button"> <i class="bi bi-check2" style="font-size: 1rem; color: white;"></i> </button>
</div>
</div>
</div>
@ -742,16 +769,19 @@
<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>
<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 class="card-body p-2">
<div class="progress mb-0" style="height: 15px;">
<div class="progress-bar progress-bar-striped bg-primary" id="rms_level" 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">RX AUDIO LEVEL - not implemented yet</p>
<div class="progress-bar progress-bar-striped bg-primary" id="dbfs_level" 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" id="dbfs_level_value">dBFS</p>
</div>
<div class="progress mb-0" style="height: 5px;">
<div class="progress-bar progress-bar-striped bg-warning" role="progressbar" style="width: 10%" aria-valuenow="10" aria-valuemin="0" aria-valuemax="100"></div>
<div class="progress-bar bg-success" role="progressbar" style="width: 80%" aria-valuenow="80" aria-valuemin="0" aria-valuemax="100"></div>
<div class="progress-bar progress-bar-striped bg-danger" role="progressbar" style="width: 10%" aria-valuenow="10" aria-valuemin="0" aria-valuemax="100"></div>
<div class="progress-bar progress-bar-striped bg-warning" role="progressbar" style="width: 1%" aria-valuenow="1" aria-valuemin="0" aria-valuemax="100"></div>
<div class="progress-bar bg-success" role="progressbar" style="width: 89%" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"></div>
<div class="progress-bar progress-bar-striped bg-warning" role="progressbar" style="width: 20%" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100"></div>
<div class="progress-bar progress-bar-striped bg-danger" role="progressbar" style="width: 29%" aria-valuenow="29" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
</div>
@ -808,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>
@ -1037,24 +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-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">
@ -1067,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>
@ -1074,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>
@ -1139,6 +1192,17 @@
<option value="zephyr">Zephyr</option>
</select>
</div>
<div class="input-group input-group-sm mb-1"> <span class="input-group-text w-50" id="basic-addon1">Waterfall Theme</span>
<select class="form-select form-select-sm w-50" id="wftheme_selector">
<option value="2">Default</option>
<option value="0">Turbo</option>
<option value="1">Fosphor</option>
<option value="3">Inferno</option>
<option value="4">Magma</option>
<option value="5">Jet</option>
<option value="6">Binary</option>
</select>
</div>
<div class="input-group input-group-sm mb-1"> <span class="input-group-text w-50" id="basic-addon1">Update channel</span>
<select class="form-select form-select-sm w-50" id="update_channel_selector">
<option value="latest">stable</option>
@ -1196,7 +1260,7 @@
</label>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Enable 563Hz mode</label>
<label class="input-group-text w-50">Enable 563Hz only mode</label>
<label class="input-group-text bg-white w-50">
<div class="form-check form-switch form-check-inline">
<input class="form-check-input" type="checkbox" id="500HzModeSwitch">
@ -1204,6 +1268,15 @@
</div>
</label>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Enable Explorer Publishing <br> (https://explorer.freedata.app) </label>
<label class="input-group-text bg-white w-50">
<div class="form-check form-switch form-check-inline">
<input class="form-check-input" type="checkbox" id="ExplorerSwitch">
<label class="form-check-label" for="ExplorerSwitch">Publish</label>
</div>
</label>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Respond to CQ</label>
<label class="input-group-text bg-white w-50">
@ -1253,4 +1326,4 @@
</div>
</body>
</html>
</html>

View file

@ -5,9 +5,12 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="script-src 'self';">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="styles.css" />
<title>FreeDATA - Live Log</title>
</head>
<body>
@ -17,10 +20,32 @@
<!-- chart.js -->
<nav class="navbar fixed-top bg-light">
<div class="container-fluid">
<input type="checkbox" class="btn-check" id="enable_filter_info" autocomplete="off" checked>
<label class="btn btn-outline-info" for="enable_filter_info">info</label>
<input type="checkbox" class="btn-check" id="enable_filter_debug" autocomplete="off">
<label class="btn btn-outline-primary" for="enable_filter_debug">debug</label>
<input type="checkbox" class="btn-check" id="enable_filter_warning" autocomplete="off">
<label class="btn btn-outline-warning" for="enable_filter_warning">warning</label>
<input type="checkbox" class="btn-check" id="enable_filter_error" autocomplete="off">
<label class="btn btn-outline-danger" for="enable_filter_error">error</label>
</div>
</nav>
<div class="container-fluid mt-5">
<div class="tableFixHead">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Timestamp</th>
<th scope="col">Type</th>
<th scope="col">Area</th>
<th scope="col">Log entry</th>
</tr>
</thead>
@ -35,6 +60,7 @@
-->
</tbody>
</table>
</div>
</div>
</body>
</html>

11
gui/src/splash.html Normal file
View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="script-src 'self';">
</head>
<body>
<img src="img/icon_cube_border.png" width="100%" height="100%">
</body>
</html>

View file

@ -8,6 +8,7 @@ body {
/*Progress bars with centered text*/
.progress {
position: relative;
transform: translateZ(0);
}
.progress span {
@ -27,4 +28,41 @@ html {
display: none;
}
#chatModuleMessage {
resize: none;
border-radius:15px;
}
#expand_textarea_label{
border: 0;
padding: 1px;
}
/* fixed border table header */
.tableFixHead {
overflow: auto;
height: 90vh;
}
.tableFixHead thead th {
position: sticky;
top: 0;
z-index: 1;
}
table {
border-collapse: collapse;
width: 100%;
}
th, td {
padding: 8px 16px;
}
th {
background:#eee;
}
/* ------ emoji picker customization --------- */
.picker {
border-radius: 10px:
}

View file

@ -68,14 +68,55 @@ Spectrum.prototype.drawFFT = function(bins) {
this.ctx.stroke();
}
Spectrum.prototype.drawSpectrum = function(bins) {
//Spectrum.prototype.drawSpectrum = function(bins) {
Spectrum.prototype.drawSpectrum = function() {
var width = this.ctx.canvas.width;
var height = this.ctx.canvas.height;
// Modification by DJ2LS
// Draw bandwidth lines
// TODO: Math not correct. But a first attempt
// it seems position is more or less equal to frequenzy by factor 10
// eg. position 150 == 1500Hz
/*
// CENTER LINE
this.ctx_wf.beginPath();
this.ctx_wf.moveTo(150,0);
this.ctx_wf.lineTo(150, height);
this.ctx_wf.lineWidth = 1;
this.ctx_wf.strokeStyle = '#8C8C8C';
this.ctx_wf.stroke()
*/
// 586Hz and 1700Hz LINES
var linePositionLow = 121.6; //150 - bandwith/20
var linePositionHigh = 178.4; //150 + bandwidth/20
var linePositionLow2 = 65; //150 - bandwith/20
var linePositionHigh2 = 235; //150 + bandwith/20
this.ctx_wf.beginPath();
this.ctx_wf.moveTo(linePositionLow,0);
this.ctx_wf.lineTo(linePositionLow, height);
this.ctx_wf.moveTo(linePositionHigh,0);
this.ctx_wf.lineTo(linePositionHigh, height);
this.ctx_wf.moveTo(linePositionLow2,0);
this.ctx_wf.lineTo(linePositionLow2, height);
this.ctx_wf.moveTo(linePositionHigh2,0);
this.ctx_wf.lineTo(linePositionHigh2, height);
this.ctx_wf.lineWidth = 1;
this.ctx_wf.strokeStyle = '#C3C3C3';
this.ctx_wf.stroke()
// ---- END OF MODIFICATION ------
// Fill with black
this.ctx.fillStyle = "white";
this.ctx.fillRect(0, 0, width, height);
//Commenting out the remainder of this code, it's not needed and unused as of 6.9.11 and saves three if statements
return;
/*
// FFT averaging
if (this.averaging > 0) {
if (!this.binsAverage || this.binsAverage.length != bins.length) {
@ -128,6 +169,12 @@ Spectrum.prototype.drawSpectrum = function(bins) {
// Copy axes from offscreen canvas
this.ctx.drawImage(this.ctx_axes.canvas, 0, 0);
*/
}
//Allow setting colormap
Spectrum.prototype.setColorMap = function(index) {
this.colormap = colormaps[index];
}
Spectrum.prototype.updateAxes = function() {
@ -196,7 +243,8 @@ Spectrum.prototype.addData = function(data) {
this.ctx_wf.fillRect(0, 0, this.wf.width, this.wf.height);
this.imagedata = this.ctx_wf.createImageData(data.length, 1);
}
this.drawSpectrum(data);
//this.drawSpectrum(data);
this.drawSpectrum();
this.addWaterfallRow(data);
this.resize();
}

View file

@ -6,15 +6,21 @@ pyserial
sounddevice
structlog
ujson
requests
chardet
colorama
ordered-set
nuitka
pyinstaller
# Development and test dependencies
autopep8
black
isort
pycodestyle
pyinstaller
pytest
pytest-cov
pytest-cover
pytest-coverage
pytest-rerunfailures
pick

View file

@ -133,7 +133,7 @@ def analyze_results(station1: list, station2: list, call_list: list):
@pytest.mark.parametrize("freedv_mode", ["datac1", "datac3"])
@pytest.mark.parametrize("n_frames_per_burst", [1]) # Higher fpb is broken.
@pytest.mark.parametrize("message_no", range(len(messages)))
@pytest.mark.flaky(reruns=2)
@pytest.mark.flaky(reruns=3)
def test_chat_text(
freedv_mode: str, n_frames_per_burst: int, message_no: int, tmp_path
):

View file

@ -15,6 +15,7 @@ Uses util_datac0.py in separate process to perform the data transfer.
"""
import multiprocessing
import numpy as np
import sys
import time
@ -62,16 +63,23 @@ def t_create_frame(frame_type: int, mycall: str, dxcall: str) -> bytearray:
dxcallsign = helpers.bytes_to_callsign(dxcallsign_bytes)
dxcallsign_crc = helpers.get_crc_24(dxcallsign)
# frame = bytearray(14)
# frame[:1] = bytes([frame_type])
# frame[1:4] = dxcallsign_crc
# frame[4:7] = mycallsign_crc
# frame[7:13] = mycallsign_bytes
session_id = np.random.bytes(1)
frame = bytearray(14)
frame[:1] = bytes([frame_type])
frame[1:4] = dxcallsign_crc
frame[4:7] = mycallsign_crc
frame[7:13] = mycallsign_bytes
frame[1:2] = session_id
frame[2:5] = dxcallsign_crc
frame[5:8] = mycallsign_crc
frame[8:14] = mycallsign_bytes
return frame
def t_create_session_close(mycall: str, dxcall: str) -> bytearray:
def t_create_session_close_old(mycall: str, dxcall: str) -> bytearray:
"""
Generate the session_close frame.
@ -85,6 +93,29 @@ def t_create_session_close(mycall: str, dxcall: str) -> bytearray:
return t_create_frame(223, mycall, dxcall)
def t_create_session_close(session_id: bytes, dxcall: str) -> bytearray:
"""
Generate the session_close frame.
:param session_id: Session to close
:type mycall: int
:return: Bytearray of the requested frame
:rtype: bytearray
"""
dxcallsign_bytes = helpers.callsign_to_bytes(dxcall)
dxcallsign = helpers.bytes_to_callsign(dxcallsign_bytes)
dxcallsign_crc = helpers.get_crc_24(dxcallsign)
# return t_create_frame(223, mycall, dxcall)
frame = bytearray(14)
frame[:1] = bytes([223])
frame[1:2] = session_id
frame[2:5] = dxcallsign_crc
return frame
def t_create_start_session(mycall: str, dxcall: str) -> bytearray:
"""
Generate the create_session frame.
@ -150,18 +181,24 @@ def t_foreign_disconnect(mycall: str, dxcall: str):
assert static.ARQ_SESSION_STATE == "connecting"
# Set up a frame from a non-associated station.
foreigncall_bytes = helpers.callsign_to_bytes("ZZ0ZZ-0")
foreigncall = helpers.bytes_to_callsign(foreigncall_bytes)
# foreigncall_bytes = helpers.callsign_to_bytes("ZZ0ZZ-0")
# foreigncall = helpers.bytes_to_callsign(foreigncall_bytes)
close_frame = t_create_session_close("ZZ0ZZ-0", "ZZ0ZZ-0")
# close_frame = t_create_session_close_old("ZZ0ZZ-0", "ZZ0ZZ-0")
open_session = create_frame[1:2]
wrong_session = np.random.bytes(1)
while wrong_session == open_session:
wrong_session = np.random.bytes(1)
close_frame = t_create_session_close(wrong_session, dxcall)
print_frame(close_frame)
assert (
helpers.check_callsign(static.DXCALLSIGN, bytes(close_frame[4:7]))[0] is False
), f"{helpers.get_crc_24(static.DXCALLSIGN)} == {bytes(close_frame[4:7])} but should be not equal."
assert (
helpers.check_callsign(foreigncall, bytes(close_frame[4:7]))[0] is True
), f"{helpers.get_crc_24(foreigncall)} != {bytes(close_frame[4:7])} but should be equal."
# assert (
# helpers.check_callsign(static.DXCALLSIGN, bytes(close_frame[4:7]))[0] is False
# ), f"{helpers.get_crc_24(static.DXCALLSIGN)} == {bytes(close_frame[4:7])} but should be not equal."
# assert (
# helpers.check_callsign(foreigncall, bytes(close_frame[4:7]))[0] is True
# ), f"{helpers.get_crc_24(foreigncall)} != {bytes(close_frame[4:7])} but should be equal."
# Send the non-associated session close frame to the TNC
tnc.received_session_close(close_frame)
@ -221,7 +258,12 @@ def t_valid_disconnect(mycall: str, dxcall: str):
assert static.ARQ_SESSION_STATE == "connecting"
# Create packet to be 'received' by this station.
close_frame = t_create_session_close(mycall=dxcall, dxcall=mycall)
# close_frame = t_create_session_close_old(mycall=dxcall, dxcall=mycall)
open_session = create_frame[1:2]
print(dxcall)
print("#####################################################")
close_frame = t_create_session_close(open_session, mycall)
print(close_frame[2:5])
print_frame(close_frame)
tnc.received_session_close(close_frame)
@ -241,7 +283,7 @@ def t_valid_disconnect(mycall: str, dxcall: str):
@pytest.mark.parametrize("mycall", ["AA1AA-2", "DE2DE-0", "E4AWQ-4"])
@pytest.mark.parametrize("dxcall", ["AA9AA-1", "DE2ED-0", "F6QWE-3"])
@pytest.mark.flaky(reruns=2)
# @pytest.mark.flaky(reruns=2)
def test_foreign_disconnect(mycall: str, dxcall: str):
proc = multiprocessing.Process(target=t_foreign_disconnect, args=(mycall, dxcall))
# print("Starting threads.")

View file

@ -44,6 +44,8 @@ def t_setup(
static.MYGRID = bytes("AA12aa", "utf-8")
static.RESPOND_TO_CQ = True
static.SSID_LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# override ARQ SESSION STATE for allowing disconnect command
static.ARQ_SESSION_STATE = "connected"
mycallsign = helpers.callsign_to_bytes(mycall)
mycallsign = helpers.bytes_to_callsign(mycallsign)
@ -187,6 +189,8 @@ def t_highsnr_arq_short_station1(
data = {"type": "arq", "command": "disconnect", "dxcallsign": dxcall}
sock.process_tnc_commands(json.dumps(data, indent=None))
time.sleep(0.5)
# override ARQ SESSION STATE for allowing disconnect command
static.ARQ_SESSION_STATE = "connected"
sock.process_tnc_commands(json.dumps(data, indent=None))
# Allow enough time for this side to process the disconnect frame.

View file

@ -41,6 +41,8 @@ def t_setup(
static.MYGRID = bytes("AA12aa", "utf-8")
static.RESPOND_TO_CQ = True
static.SSID_LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# override ARQ SESSION STATE for allowing disconnect command
static.ARQ_SESSION_STATE = "connected"
mycallsign = helpers.callsign_to_bytes(mycall)
mycallsign = helpers.bytes_to_callsign(mycallsign)

View file

@ -164,7 +164,8 @@ def t_datac0_1(
break
time.sleep(0.1)
log.info("station1, first")
# override ARQ SESSION STATE for allowing disconnect command
static.ARQ_SESSION_STATE = "connected"
data = {"type": "arq", "command": "disconnect", "dxcallsign": dxcall}
sock.process_tnc_commands(json.dumps(data, indent=None))
time.sleep(0.5)
@ -295,6 +296,6 @@ def t_datac0_2(
assert item in str(
sock.SOCKET_QUEUE.queue
), f"{item} not found in {str(sock.SOCKET_QUEUE.queue)}"
assert '"arq":"session","status":"close"' 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)
log.warning("station2: Exiting!")

View file

@ -170,7 +170,8 @@ def t_datac0_1(
log.debug("STOP test, resetting DX callsign")
static.DXCALLSIGN = orig_dxcall
static.DXCALLSIGN_CRC = helpers.get_crc_24(static.DXCALLSIGN)
# override ARQ SESSION STATE for allowing disconnect command
static.ARQ_SESSION_STATE = "connected"
data = {"type": "arq", "command": "disconnect", "dxcallsign": dxcall}
sock.process_tnc_commands(json.dumps(data, indent=None))
time.sleep(0.5)
@ -286,7 +287,10 @@ def t_datac0_2(
# Allow enough time for this side to receive the disconnect frame.
timeout = time.time() + timeout_duration
while '"arq":"session","status":"close"' not in str(sock.SOCKET_QUEUE.queue):
while '"arq":"session", "status":"close"' not in str(sock.SOCKET_QUEUE.queue):
if time.time() > timeout:
log.warning("station2", TIMEOUT=True, queue=str(sock.SOCKET_QUEUE.queue))
break
@ -301,6 +305,6 @@ def t_datac0_2(
assert item not in str(
sock.SOCKET_QUEUE.queue
), f"{item} found in {str(sock.SOCKET_QUEUE.queue)}"
assert '"arq":"session","status":"close"' 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)
log.warning("station2: Exiting!")

View file

@ -31,7 +31,7 @@ def get_audio_devices():
sd._terminate()
sd._initialize()
log.debug("[AUD] get_audio_devices")
# log.debug("[AUD] get_audio_devices")
with multiprocessing.Manager() as manager:
proxy_input_devices = manager.list()
proxy_output_devices = manager.list()
@ -42,8 +42,9 @@ def get_audio_devices():
proc.start()
proc.join()
#log.debug("[AUD] get_audio_devices: input_devices:", list=f"{proxy_input_devices}")
#log.debug("[AUD] get_audio_devices: output_devices:", list=f"{proxy_output_devices}")
# additional logging for audio devices
# log.debug("[AUD] get_audio_devices: input_devices:", list=f"{proxy_input_devices}")
# log.debug("[AUD] get_audio_devices: output_devices:", list=f"{proxy_output_devices}")
return list(proxy_input_devices), list(proxy_output_devices)
@ -52,7 +53,10 @@ def device_crc(device) -> str:
crc_hwid = crc_algorithm(bytes(f"{device}", encoding="utf-8"))
crc_hwid = crc_hwid.to_bytes(2, byteorder="big")
crc_hwid = crc_hwid.hex()
return f"{device['name']} [{crc_hwid}]"
hostapi_name = sd.query_hostapis(device['hostapi'])['name']
return f"{device['name']} [{hostapi_name}] [{crc_hwid}]"
def fetch_audio_devices(input_devices, output_devices):

View file

@ -23,7 +23,8 @@ class FREEDV_MODE(Enum):
"""
Enumeration for codec2 modes and names
"""
sig0 = 14
sig1 = 14
datac0 = 14
datac1 = 10
datac3 = 12
@ -103,6 +104,9 @@ api.freedv_open_advanced.restype = ctypes.c_void_p
api.freedv_get_bits_per_modem_frame.argtype = [ctypes.c_void_p] # type: ignore
api.freedv_get_bits_per_modem_frame.restype = ctypes.c_int
api.freedv_get_modem_extended_stats.argtype = [ctypes.c_void_p, ctypes.c_void_p]
api.freedv_get_modem_extended_stats.restype = ctypes.c_int
api.freedv_nin.argtype = [ctypes.c_void_p] # type: ignore
api.freedv_nin.restype = ctypes.c_int
@ -208,8 +212,8 @@ api.FREEDV_MODE_FSK_LDPC_1_ADV.tone_spacing = 200
api.FREEDV_MODE_FSK_LDPC_1_ADV.codename = "H_256_512_4".encode("utf-8") # code word
# ------- MODEM STATS STRUCTURES
MODEM_STATS_NC_MAX = 50 + 1
MODEM_STATS_NR_MAX = 160
MODEM_STATS_NC_MAX = 50 + 1 * 2
MODEM_STATS_NR_MAX = 160 * 2
MODEM_STATS_ET_MAX = 8
MODEM_STATS_EYE_IND_MAX = 160
MODEM_STATS_NSPEC = 512
@ -233,10 +237,12 @@ class MODEMSTATS(ctypes.Structure):
("pre", ctypes.c_int),
("post", ctypes.c_int),
("uw_fails", ctypes.c_int),
("rx_eye", (ctypes.c_float * MODEM_STATS_ET_MAX) * MODEM_STATS_EYE_IND_MAX),
("neyetr", ctypes.c_int), # How many eye traces are plotted
("neyesamp", ctypes.c_int), # How many samples in the eye diagram
("f_est", (ctypes.c_float * MODEM_STATS_MAX_F_EST)),
("fft_buf", (ctypes.c_float * MODEM_STATS_NSPEC * 2)),
("fft_cfg", ctypes.c_void_p)
]

33
tnc/config.ini Normal file
View file

@ -0,0 +1,33 @@
[NETWORK]
#network settings
tncport = 3000
[STATION]
#station settings
mycall = DJ2LS-4
mygrid = JN48cs
ssid_list = [0,1,2,3,4,5]
[AUDIO]
#audio settings
rx = hw:2,0
tx = USB Audio CODEC
txaudiolevel = 120
[RADIO]
#radio settings
radiocontrol = rigctld
rigctld_ip = 127.0.0.1
rigctld_port = 4532
[TNC]
#tnc settings
scatter = True
fft = True
narrowband = False
fmin = -50.0
fmax = 50.0
qrv = True
rxbuffersize = 16
explorer = False

View file

@ -7,13 +7,18 @@ class CONFIG:
"""
def __init__(self):
def __init__(self, configfile: str):
# set up logger
self.log = structlog.get_logger("CONFIG")
# init configparser
self.config = configparser.ConfigParser(inline_comment_prefixes="#", allow_no_value=True)
self.config_name = "config.ini"
try:
self.config_name = configfile
except Exception:
self.config_name = "config.ini"
self.log.info("[CFG] logfile init", file=self.config_name)
@ -45,7 +50,8 @@ class CONFIG:
self.config['STATION'] = {'#Station settings': None,
'mycall': data[1],
'mygrid': data[2]
'mygrid': data[2],
'ssid_list': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # list(data[26])
}
self.config['AUDIO'] = {'#Audio settings': None,
@ -56,14 +62,15 @@ class CONFIG:
}
self.config['RADIO'] = {'#Radio settings': None,
'radiocontrol': data[13],
'devicename': data[5],
'deviceport': data[6],
'serialspeed': data[7],
'pttprotocol': data[8],
'pttport': data[9],
'data_bits': data[10],
'stop_bits': data[11],
'handshake': data[12],
# TODO: disabled because we dont need these settings anymore
#'devicename': data[5],
#'deviceport': data[6],
#'serialspeed': data[7],
#'pttprotocol': data[8],
#'pttport': data[9],
#'data_bits': data[10],
#'stop_bits': data[11],
#'handshake': data[12],
'rigctld_ip': data[14],
'rigctld_port': data[15]
}
@ -74,7 +81,8 @@ class CONFIG:
'fmin': data[19],
'fmax': data[20],
'qrv': data[23],
'rxbuffersize': data[24]
'rxbuffersize': data[24],
'explorer': data[25]
}
try:
with open(self.config_name, 'w') as configfile:

View file

@ -93,7 +93,7 @@ class DAEMON:
"[DMN] update_audio_devices: Exception gathering audio devices:",
e=err1,
)
time.sleep(1)
threading.Event().wait(1)
def update_serial_devices(self):
"""
@ -114,7 +114,7 @@ class DAEMON:
)
static.SERIAL_DEVICES = serial_devices
time.sleep(1)
threading.Event().wait(1)
except Exception as err1:
self.log.error(
"[DMN] update_serial_devices: Exception gathering serial devices:",
@ -156,7 +156,8 @@ class DAEMON:
# data[22] tx-audio-level
# data[23] respond_to_cq
# data[24] rx_buffer_size
# data[25] explorer
# data[26] ssid_list
if data[0] == "STARTTNC":
self.log.warning("[DMN] Starting TNC", rig=data[5], port=data[6])
@ -248,6 +249,17 @@ class DAEMON:
options.append("--rx-buffer-size")
options.append(data[24])
if data[25] == "True":
options.append("--explorer")
# wen want our ssid like this: --ssid 1 2 3 4
ssid_list = ""
for i in data[26]:
ssid_list += str(i) + " "
options.append("--ssid")
options.append(ssid_list)
# safe data to config file
config.write_entire_config(data)
@ -317,9 +329,11 @@ class DAEMON:
# check how we want to control the radio
if radiocontrol == "direct":
import rig
print("direct hamlib support deprecated - not usable anymore")
sys.exit(1)
elif radiocontrol == "rigctl":
import rigctl as rig
print("rigctl support deprecated - not usable anymore")
sys.exit(1)
elif radiocontrol == "rigctld":
import rigctld as rig
else:
@ -402,7 +416,7 @@ if __name__ == "__main__":
mainlog.error("[DMN] logger init error", exception=err)
# init config
config = config.CONFIG()
config = config.CONFIG("config.ini")
try:
mainlog.info("[DMN] Starting TCP/IP socket", port=static.DAEMONPORT)
@ -429,4 +443,4 @@ if __name__ == "__main__":
version=static.VERSION,
)
while True:
time.sleep(1)
threading.Event().wait(1)

File diff suppressed because it is too large Load diff

68
tnc/explorer.py Normal file
View file

@ -0,0 +1,68 @@
# -*- coding: UTF-8 -*-
"""
Created on 05.11.23
@author: DJ2LS
"""
# pylint: disable=invalid-name, line-too-long, c-extension-no-member
# pylint: disable=import-outside-toplevel, attribute-defined-outside-init
import requests
import threading
import time
import ujson as json
import structlog
import static
log = structlog.get_logger("explorer")
class explorer():
def __init__(self):
self.explorer_url = "https://explorer.freedata.app/api.php"
self.publish_interval = 120
self.interval_thread = threading.Thread(target=self.interval, name="interval", daemon=True)
self.interval_thread.start()
def interval(self):
while True:
self.push()
threading.Event().wait(self.publish_interval)
def push(self):
frequency = 0 if static.HAMLIB_FREQUENCY is None else static.HAMLIB_FREQUENCY
band = "USB"
callsign = str(static.MYCALLSIGN, "utf-8")
gridsquare = str(static.MYGRID, "utf-8")
version = str(static.VERSION)
bandwidth = str(static.LOW_BANDWIDTH_MODE)
beacon = str(static.BEACON_STATE)
log.info("[EXPLORER] publish", frequency=frequency, band=band, callsign=callsign, gridsquare=gridsquare, version=version, bandwidth=bandwidth)
headers = {"Content-Type": "application/json"}
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")
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, "timestamp": timestamp})
except Exception as e:
log.debug("[EXPLORER] not publishing station", e=e)
station_data = json.dumps(station_data)
try:
response = requests.post(self.explorer_url, json=station_data, headers=headers)
# print(response.status_code)
# print(response.content)
except Exception as e:
log.warning("[EXPLORER] connection lost")

View file

@ -7,7 +7,7 @@ block_cipher = None
daemon_a = Analysis(['daemon.py'],
pathex=[],
binaries=[],
datas=[( './lib/hamlib/linux/python3.8/site-packages/libhamlib.so.4', '.' )],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},

View file

@ -5,10 +5,12 @@ Created on Fri Dec 25 21:25:14 2020
@author: DJ2LS
"""
import time
from datetime import datetime,timezone
import crcengine
import static
import structlog
import numpy as np
import threading
log = structlog.get_logger("helpers")
@ -24,7 +26,7 @@ def wait(seconds: float) -> bool:
timeout = time.time() + seconds
while time.time() < timeout:
time.sleep(0.01)
threading.Event().wait(0.01)
return True
@ -130,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:
@ -314,7 +316,25 @@ 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):
"""
Funktion to check if we received the correct session id
Args:
id: our own session id
id_to_check: The session id byte we want to check
Returns:
True
False
"""
if id_to_check == b'\x00':
return False
log.debug("[HLP] check_sessionid: Checking:", ownid=id, check=id_to_check)
return id == id_to_check
def encode_grid(grid):
@ -374,11 +394,7 @@ def decode_grid(b_code_word: bytes):
int_val = int(code_word & 0b111111111)
int_first, int_sec = divmod(int_val, 18)
# int_first = int_val // 18
# int_sec = int_val % 18
grid = chr(int(int_first) + 65) + chr(int(int_sec) + 65) + grid
return grid
return chr(int(int_first) + 65) + chr(int(int_sec) + 65) + grid
def encode_call(call):
@ -428,3 +444,21 @@ def decode_call(b_code_word: bytes):
call = call[:-1] + ssid # remove the last char from call and replace with SSID
return call
def snr_to_bytes(snr):
"""create a byte from snr value """
# make sure we have onl 1 byte snr
# min max = -12.7 / 12.7
# enough for detecting if a channel is good or bad
snr = snr * 10
snr = np.clip(snr, -127, 127)
snr = int(snr).to_bytes(1, byteorder='big', signed=True)
return snr
def snr_from_bytes(snr):
"""create int from snr byte"""
snr = int.from_bytes(snr, byteorder='big', signed=True)
snr = snr / 10
return snr

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -1,3 +0,0 @@
# Hamlib Python binding
we need to copy "libhamlib.so.4.0.4" from build_dir/lib/ to this path, rename it to "libhamlib.so.4 and add it to source directory of tnc and daemon binary

View file

@ -1,41 +0,0 @@
# _Hamlib.la - a libtool library file
# Generated by libtool (GNU libtool) 2.4.6 Debian-2.4.6-9
#
# Please DO NOT delete this file!
# It is necessary for linking the library.
# The name that we can dlopen(3).
dlname='_Hamlib.so'
# Names of this library.
library_names='_Hamlib.so _Hamlib.so _Hamlib.so'
# The name of the static archive.
old_library='_Hamlib.a'
# Linker flags that cannot go in dependency_libs.
inherited_linker_flags=' -pthread'
# Libraries that this one depends upon.
dependency_libs=' /home/dj2ls/hamlib4.4/lib/libhamlib.la -ldl -lm -lusb-1.0 -L/usr/lib -lpython3.8'
# Names of additional weak libraries provided by this library
weak_library_names=''
# Version information for _Hamlib.
current=0
age=0
revision=0
# Is this an already installed library?
installed=yes
# Should we warn about portability when linking against -modules?
shouldnotlink=yes
# Files to dlopen/dlpreopen
dlopen=''
dlpreopen=''
# Directory that this library needs to be installed in:
libdir='/home/dj2ls/hamlib4.4/lib/python3.8/site-packages'

View file

@ -1 +0,0 @@
# hamlib win32 4.4

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1 +0,0 @@
# hamlib win64 4.4

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

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

View file

@ -7,6 +7,14 @@ Created on Tue Dec 22 16:58:45 2020
main module for running the tnc
"""
# run tnc self test on startup before we are doing other things
# import selftest
# selftest.TEST()
# continue if we passed the test
import argparse
import multiprocessing
import os
@ -22,10 +30,11 @@ import log_handler
import modem
import static
import structlog
import explorer
import json
log = structlog.get_logger("main")
def signal_handler(sig, frame):
"""
a signal handler, which closes the network/socket when closing the application
@ -49,12 +58,28 @@ if __name__ == "__main__":
# --------------------------------------------GET PARAMETER INPUTS
PARSER = argparse.ArgumentParser(description="FreeDATA TNC")
#PARSER.add_argument(
# "--use-config",
# dest="configfile",
# action="store_true",
# help="Use the default config file config.ini",
#)
PARSER.add_argument(
"--use-config",
dest="configfile",
action="store_true",
default=False,
type=str,
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(
"--mycall",
dest="mycall",
@ -68,7 +93,7 @@ if __name__ == "__main__":
nargs="*",
default=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
help="SSID list we are responding to",
type=str,
type=int,
)
PARSER.add_argument(
"--mygrid",
@ -82,14 +107,14 @@ if __name__ == "__main__":
dest="audio_input_device",
default=0,
help="listening sound card",
type=int,
type=str,
)
PARSER.add_argument(
"--tx",
dest="audio_output_device",
default=0,
help="transmitting sound card",
type=int,
type=str,
)
PARSER.add_argument(
"--port",
@ -246,81 +271,131 @@ if __name__ == "__main__":
help="Set the maximum size of rx buffer.",
type=int,
)
PARSER.add_argument(
"--explorer",
dest="enable_explorer",
action="store_true",
help="Enable sending tnc data to https://explorer.freedata.app",
)
ARGS = PARSER.parse_args()
if ARGS.configfile:
# init config
config = config.CONFIG().read_config()
# set save to folder state for allowing downloading files to local file system
static.ARQ_SAVE_TO_FOLDER = ARGS.savetofolder
if not ARGS.configfile:
# additional step for being sure our callsign is correctly
# in case we are not getting a station ssid
# then we are forcing a station ssid = 0
mycallsign = bytes(config['STATION']['mycall'], "utf-8")
mycallsign = helpers.callsign_to_bytes(mycallsign)
static.MYCALLSIGN = helpers.bytes_to_callsign(mycallsign)
static.MYCALLSIGN_CRC = helpers.get_crc_24(static.MYCALLSIGN)
try:
mycallsign = bytes(ARGS.mycall.upper(), "utf-8")
mycallsign = helpers.callsign_to_bytes(mycallsign)
static.MYCALLSIGN = helpers.bytes_to_callsign(mycallsign)
static.MYCALLSIGN_CRC = helpers.get_crc_24(static.MYCALLSIGN)
static.SSID_LIST = ARGS.ssid_list
# check if own ssid is always part of ssid list
own_ssid = int(static.MYCALLSIGN.split(b"-")[1])
if own_ssid not in static.SSID_LIST:
static.SSID_LIST.append(own_ssid)
static.SSID_LIST = [] ####
static.MYGRID = bytes(config['STATION']['mygrid'], "utf-8")
static.AUDIO_INPUT_DEVICE = int(config['AUDIO']['rx'])
static.AUDIO_OUTPUT_DEVICE = int(config['AUDIO']['tx'])
static.PORT = int(config['NETWORK']['tncport'])
static.HAMLIB_DEVICE_NAME = config['RADIO']['devicename']
static.HAMLIB_DEVICE_PORT = config['RADIO']['deviceport']
static.HAMLIB_PTT_TYPE = config['RADIO']['pttprotocol']
static.HAMLIB_PTT_PORT = config['RADIO']['pttport']
static.HAMLIB_SERIAL_SPEED = str(config['RADIO']['serialspeed'])
static.HAMLIB_DATA_BITS = str(config['RADIO']['data_bits'])
static.HAMLIB_STOP_BITS = str(config['RADIO']['stop_bits'])
static.HAMLIB_HANDSHAKE = config['RADIO']['handshake']
static.HAMLIB_RADIOCONTROL = config['RADIO']['radiocontrol']
static.HAMLIB_RIGCTLD_IP = config['RADIO']['rigctld_ip']
static.HAMLIB_RIGCTLD_PORT = str(config['RADIO']['rigctld_port'])
static.ENABLE_SCATTER = config['TNC']['scatter']
static.ENABLE_FFT = config['TNC']['fft']
static.ENABLE_FSK = False
static.LOW_BANDWIDTH_MODE = config['TNC']['narrowband']
static.TUNING_RANGE_FMIN = float(config['TNC']['fmin'])
static.TUNING_RANGE_FMAX = float(config['TNC']['fmax'])
static.TX_AUDIO_LEVEL = config['AUDIO']['txaudiolevel']
static.RESPOND_TO_CQ = config['TNC']['qrv']
static.RX_BUFFER_SIZE = config['TNC']['rxbuffersize']
static.MYGRID = bytes(ARGS.mygrid, "utf-8")
# check if we have an int or str as device name
try:
static.AUDIO_INPUT_DEVICE = int(ARGS.audio_input_device)
except ValueError:
static.AUDIO_INPUT_DEVICE = ARGS.audio_input_device
try:
static.AUDIO_OUTPUT_DEVICE = int(ARGS.audio_output_device)
except ValueError:
static.AUDIO_OUTPUT_DEVICE = ARGS.audio_output_device
static.PORT = ARGS.socket_port
static.HAMLIB_DEVICE_NAME = ARGS.hamlib_device_name
static.HAMLIB_DEVICE_PORT = ARGS.hamlib_device_port
static.HAMLIB_PTT_TYPE = ARGS.hamlib_ptt_type
static.HAMLIB_PTT_PORT = ARGS.hamlib_ptt_port
static.HAMLIB_SERIAL_SPEED = str(ARGS.hamlib_serialspeed)
static.HAMLIB_DATA_BITS = str(ARGS.hamlib_data_bits)
static.HAMLIB_STOP_BITS = str(ARGS.hamlib_stop_bits)
static.HAMLIB_HANDSHAKE = ARGS.hamlib_handshake
static.HAMLIB_RADIOCONTROL = ARGS.hamlib_radiocontrol
static.HAMLIB_RIGCTLD_IP = ARGS.rigctld_ip
static.HAMLIB_RIGCTLD_PORT = str(ARGS.rigctld_port)
static.ENABLE_SCATTER = ARGS.send_scatter
static.ENABLE_FFT = ARGS.send_fft
static.ENABLE_FSK = ARGS.enable_fsk
static.LOW_BANDWIDTH_MODE = ARGS.low_bandwidth_mode
static.TUNING_RANGE_FMIN = ARGS.tuning_range_fmin
static.TUNING_RANGE_FMAX = ARGS.tuning_range_fmax
static.TX_AUDIO_LEVEL = ARGS.tx_audio_level
static.RESPOND_TO_CQ = ARGS.enable_respond_to_cq
static.RX_BUFFER_SIZE = ARGS.rx_buffer_size
static.ENABLE_EXPLORER = ARGS.enable_explorer
except Exception as e:
log.error("[DMN] Error reading config file", exception=e)
else:
# additional step for being sure our callsign is correctly
# in case we are not getting a station ssid
# then we are forcing a station ssid = 0
mycallsign = bytes(ARGS.mycall.upper(), "utf-8")
mycallsign = helpers.callsign_to_bytes(mycallsign)
static.MYCALLSIGN = helpers.bytes_to_callsign(mycallsign)
static.MYCALLSIGN_CRC = helpers.get_crc_24(static.MYCALLSIGN)
configfile = ARGS.configfile
# init config
config = config.CONFIG(configfile).read_config()
try:
# additional step for being sure our callsign is correctly
# in case we are not getting a station ssid
# then we are forcing a station ssid = 0
mycallsign = bytes(config['STATION']['mycall'], "utf-8")
mycallsign = helpers.callsign_to_bytes(mycallsign)
static.MYCALLSIGN = helpers.bytes_to_callsign(mycallsign)
static.MYCALLSIGN_CRC = helpers.get_crc_24(static.MYCALLSIGN)
static.SSID_LIST = ARGS.ssid_list
static.MYGRID = bytes(ARGS.mygrid, "utf-8")
static.AUDIO_INPUT_DEVICE = ARGS.audio_input_device
static.AUDIO_OUTPUT_DEVICE = ARGS.audio_output_device
static.PORT = ARGS.socket_port
static.HAMLIB_DEVICE_NAME = ARGS.hamlib_device_name
static.HAMLIB_DEVICE_PORT = ARGS.hamlib_device_port
static.HAMLIB_PTT_TYPE = ARGS.hamlib_ptt_type
static.HAMLIB_PTT_PORT = ARGS.hamlib_ptt_port
static.HAMLIB_SERIAL_SPEED = str(ARGS.hamlib_serialspeed)
static.HAMLIB_DATA_BITS = str(ARGS.hamlib_data_bits)
static.HAMLIB_STOP_BITS = str(ARGS.hamlib_stop_bits)
static.HAMLIB_HANDSHAKE = ARGS.hamlib_handshake
static.HAMLIB_RADIOCONTROL = ARGS.hamlib_radiocontrol
static.HAMLIB_RIGCTLD_IP = ARGS.rigctld_ip
static.HAMLIB_RIGCTLD_PORT = str(ARGS.rigctld_port)
static.ENABLE_SCATTER = ARGS.send_scatter
static.ENABLE_FFT = ARGS.send_fft
static.ENABLE_FSK = ARGS.enable_fsk
static.LOW_BANDWIDTH_MODE = ARGS.low_bandwidth_mode
static.TUNING_RANGE_FMIN = ARGS.tuning_range_fmin
static.TUNING_RANGE_FMAX = ARGS.tuning_range_fmax
static.TX_AUDIO_LEVEL = ARGS.tx_audio_level
static.RESPOND_TO_CQ = ARGS.enable_respond_to_cq
static.RX_BUFFER_SIZE = ARGS.rx_buffer_size
#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:
static.AUDIO_INPUT_DEVICE = int(config['AUDIO']['rx'])
except ValueError:
static.AUDIO_INPUT_DEVICE = config['AUDIO']['rx']
try:
static.AUDIO_OUTPUT_DEVICE = int(config['AUDIO']['tx'])
except ValueError:
static.AUDIO_OUTPUT_DEVICE = config['AUDIO']['tx']
static.PORT = int(config['NETWORK']['tncport'])
# TODO: disabled because we don't need these settings anymore.
#static.HAMLIB_DEVICE_NAME = config['RADIO']['devicename']
#static.HAMLIB_DEVICE_PORT = config['RADIO']['deviceport']
#static.HAMLIB_PTT_TYPE = config['RADIO']['pttprotocol']
#static.HAMLIB_PTT_PORT = config['RADIO']['pttport']
#static.HAMLIB_SERIAL_SPEED = str(config['RADIO']['serialspeed'])
#static.HAMLIB_DATA_BITS = str(config['RADIO']['data_bits'])
#static.HAMLIB_STOP_BITS = str(config['RADIO']['stop_bits'])
#static.HAMLIB_HANDSHAKE = config['RADIO']['handshake']
static.HAMLIB_RADIOCONTROL = config['RADIO']['radiocontrol']
static.HAMLIB_RIGCTLD_IP = config['RADIO']['rigctld_ip']
static.HAMLIB_RIGCTLD_PORT = str(config['RADIO']['rigctld_port'])
static.ENABLE_SCATTER = config['TNC']['scatter'] in ["True", "true", True]
static.ENABLE_FFT = config['TNC']['fft'] in ["True", "true", True]
static.ENABLE_FSK = False
static.LOW_BANDWIDTH_MODE = config['TNC']['narrowband'] in ["True", "true", True]
static.TUNING_RANGE_FMIN = float(config['TNC']['fmin'])
static.TUNING_RANGE_FMAX = float(config['TNC']['fmax'])
static.TX_AUDIO_LEVEL = config['AUDIO']['txaudiolevel']
static.RESPOND_TO_CQ = config['TNC']['qrv'] in ["True", "true", True]
static.RX_BUFFER_SIZE = int(config['TNC']['rxbuffersize'])
static.ENABLE_EXPLORER = config['TNC']['explorer'] in ["True", "true", True]
except KeyError as e:
log.warning("[CFG] Error reading config file near", key=str(e))
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
@ -358,6 +433,11 @@ if __name__ == "__main__":
# start modem
modem = modem.RF()
# optionally start explorer module
if static.ENABLE_EXPLORER:
log.info("[EXPLORER] Publishing to https://explorer.freedata.app", state=static.ENABLE_EXPLORER)
explorer = explorer.explorer()
# --------------------------------------------START CMD SERVER
try:
log.info("[TNC] Starting TCP/IP socket", port=static.PORT)
@ -375,4 +455,4 @@ if __name__ == "__main__":
log.error("[TNC] Starting TCP/IP socket failed", port=static.PORT, e=err)
sys.exit(1)
while True:
time.sleep(1)
threading.Event().wait(1)

View file

@ -5,6 +5,7 @@ Created on Wed Dec 23 07:04:24 2020
@author: DJ2LS
"""
# pylint: disable=invalid-name, line-too-long, c-extension-no-member
# pylint: disable=import-outside-toplevel
@ -15,15 +16,16 @@ import sys
import threading
import time
from collections import deque
import wave
import codec2
import itertools
import numpy as np
import sock
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 = ""
@ -32,10 +34,19 @@ TXCHANNEL = ""
static.TRANSMITTING = False
# Receive only specific modes to reduce CPU load
RECEIVE_SIG0 = True
RECEIVE_SIG1 = False
RECEIVE_DATAC1 = False
RECEIVE_DATAC3 = False
RECEIVE_FSK_LDPC_1 = False
# state buffer
SIG0_DATAC0_STATE = []
SIG1_DATAC0_STATE = []
DAT0_DATAC1_STATE = []
DAT0_DATAC3_STATE = []
class RF:
"""Class to encapsulate interactions between the audio device and codec2"""
@ -58,6 +69,7 @@ class RF:
self.AUDIO_CHANNELS = 1
self.MODE = 0
# 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/99
@ -81,111 +93,68 @@ class RF:
self.fft_data = bytes()
# Open codec2 instances
# Datac0 - control frames
self.datac0_freedv = ctypes.cast(
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), ctypes.c_void_p
)
self.c_lib.freedv_set_tuning_range(
self.datac0_freedv,
ctypes.c_float(static.TUNING_RANGE_FMIN),
ctypes.c_float(static.TUNING_RANGE_FMAX),
)
self.datac0_bytes_per_frame = int(
codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv) / 8
)
self.datac0_bytes_out = ctypes.create_string_buffer(self.datac0_bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(self.datac0_freedv, 1)
self.datac0_buffer = codec2.audio_buffer(2 * self.AUDIO_FRAMES_PER_BUFFER_RX)
# Additional Datac0-specific information - these are not referenced anywhere else.
# self.datac0_payload_per_frame = self.datac0_bytes_per_frame - 2
# self.datac0_n_nom_modem_samples = self.c_lib.freedv_get_n_nom_modem_samples(
# self.datac0_freedv
# )
# self.datac0_n_tx_modem_samples = self.c_lib.freedv_get_n_tx_modem_samples(
# self.datac0_freedv
# )
# self.datac0_n_tx_preamble_modem_samples = (
# self.c_lib.freedv_get_n_tx_preamble_modem_samples(self.datac0_freedv)
# )
# self.datac0_n_tx_postamble_modem_samples = (
# self.c_lib.freedv_get_n_tx_postamble_modem_samples(self.datac0_freedv)
# )
# DATAC0
# SIGNALLING MODE 0 - Used for Connecting - Payload 14 Bytes
self.sig0_datac0_freedv, \
self.sig0_datac0_bytes_per_frame, \
self.sig0_datac0_bytes_out, \
self.sig0_datac0_buffer, \
self.sig0_datac0_nin = \
self.init_codec2_mode(codec2.api.FREEDV_MODE_DATAC0, None)
# Datac1 - higher-bandwidth data frames
self.datac1_freedv = ctypes.cast(
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), ctypes.c_void_p
)
self.c_lib.freedv_set_tuning_range(
self.datac1_freedv,
ctypes.c_float(static.TUNING_RANGE_FMIN),
ctypes.c_float(static.TUNING_RANGE_FMAX),
)
self.datac1_bytes_per_frame = int(
codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv) / 8
)
self.datac1_bytes_out = ctypes.create_string_buffer(self.datac1_bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(self.datac1_freedv, 1)
self.datac1_buffer = codec2.audio_buffer(2 * self.AUDIO_FRAMES_PER_BUFFER_RX)
# DATAC0
# SIGNALLING MODE 1 - Used for ACK/NACK - Payload 5 Bytes
self.sig1_datac0_freedv, \
self.sig1_datac0_bytes_per_frame, \
self.sig1_datac0_bytes_out, \
self.sig1_datac0_buffer, \
self.sig1_datac0_nin = \
self.init_codec2_mode(codec2.api.FREEDV_MODE_DATAC0, None)
# Datac3 - lower-bandwidth data frames
self.datac3_freedv = ctypes.cast(
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), ctypes.c_void_p
)
self.c_lib.freedv_set_tuning_range(
self.datac3_freedv,
ctypes.c_float(static.TUNING_RANGE_FMIN),
ctypes.c_float(static.TUNING_RANGE_FMAX),
)
self.datac3_bytes_per_frame = int(
codec2.api.freedv_get_bits_per_modem_frame(self.datac3_freedv) / 8
)
self.datac3_bytes_out = ctypes.create_string_buffer(self.datac3_bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(self.datac3_freedv, 1)
self.datac3_buffer = codec2.audio_buffer(2 * self.AUDIO_FRAMES_PER_BUFFER_RX)
# FSK Long-distance Parity Code 0 - data frames
self.fsk_ldpc_freedv_0 = ctypes.cast(
codec2.api.freedv_open_advanced(
# DATAC1
self.dat0_datac1_freedv, \
self.dat0_datac1_bytes_per_frame, \
self.dat0_datac1_bytes_out, \
self.dat0_datac1_buffer, \
self.dat0_datac1_nin = \
self.init_codec2_mode(codec2.api.FREEDV_MODE_DATAC1, None)
# DATAC3
self.dat0_datac3_freedv, \
self.dat0_datac3_bytes_per_frame, \
self.dat0_datac3_bytes_out, \
self.dat0_datac3_buffer, \
self.dat0_datac3_nin = \
self.init_codec2_mode(codec2.api.FREEDV_MODE_DATAC3, None)
# FSK LDPC - 0
self.fsk_ldpc_freedv_0, \
self.fsk_ldpc_bytes_per_frame_0, \
self.fsk_ldpc_bytes_out_0, \
self.fsk_ldpc_buffer_0, \
self.fsk_ldpc_nin_0 = \
self.init_codec2_mode(
codec2.api.FREEDV_MODE_FSK_LDPC,
ctypes.byref(codec2.api.FREEDV_MODE_FSK_LDPC_0_ADV),
),
ctypes.c_void_p,
)
self.fsk_ldpc_bytes_per_frame_0 = int(
codec2.api.freedv_get_bits_per_modem_frame(self.fsk_ldpc_freedv_0) / 8
)
self.fsk_ldpc_bytes_out_0 = ctypes.create_string_buffer(
self.fsk_ldpc_bytes_per_frame_0
)
# codec2.api.freedv_set_frames_per_burst(self.fsk_ldpc_freedv_0, 1)
self.fsk_ldpc_buffer_0 = codec2.audio_buffer(self.AUDIO_FRAMES_PER_BUFFER_RX)
codec2.api.FREEDV_MODE_FSK_LDPC_0_ADV
)
# FSK Long-distance Parity Code 1 - data frames
self.fsk_ldpc_freedv_1 = ctypes.cast(
codec2.api.freedv_open_advanced(
# FSK LDPC - 1
self.fsk_ldpc_freedv_1, \
self.fsk_ldpc_bytes_per_frame_1, \
self.fsk_ldpc_bytes_out_1, \
self.fsk_ldpc_buffer_1, \
self.fsk_ldpc_nin_1 = \
self.init_codec2_mode(
codec2.api.FREEDV_MODE_FSK_LDPC,
ctypes.byref(codec2.api.FREEDV_MODE_FSK_LDPC_1_ADV),
),
ctypes.c_void_p,
)
self.fsk_ldpc_bytes_per_frame_1 = int(
codec2.api.freedv_get_bits_per_modem_frame(self.fsk_ldpc_freedv_1) / 8
)
self.fsk_ldpc_bytes_out_1 = ctypes.create_string_buffer(
self.fsk_ldpc_bytes_per_frame_1
)
# codec2.api.freedv_set_frames_per_burst(self.fsk_ldpc_freedv_0, 1)
self.fsk_ldpc_buffer_1 = codec2.audio_buffer(self.AUDIO_FRAMES_PER_BUFFER_RX)
# initial nin values
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
self.fsk_ldpc_nin_0 = codec2.api.freedv_nin(self.fsk_ldpc_freedv_0)
self.fsk_ldpc_nin_1 = codec2.api.freedv_nin(self.fsk_ldpc_freedv_1)
# self.log.debug("[MDM] RF: ",datac0_nin=self.datac0_nin)
codec2.api.FREEDV_MODE_FSK_LDPC_1_ADV
)
# INIT TX MODES
self.freedv_datac0_tx = open_codec2_instance(14)
self.freedv_datac1_tx = open_codec2_instance(10)
self.freedv_datac3_tx = open_codec2_instance(12)
# --------------------------------------------CREATE PYAUDIO INSTANCE
if not TESTMODE:
try:
@ -242,10 +211,13 @@ class RF:
# --------------------------------------------INIT AND OPEN HAMLIB
# Check how we want to control the radio
# TODO: deprecated feature - we can remove this possibly
if static.HAMLIB_RADIOCONTROL == "direct":
import rig
print("direct hamlib support deprecated - not usable anymore")
sys.exit(1)
elif static.HAMLIB_RADIOCONTROL == "rigctl":
import rigctl as rig
print("rigctl support deprecated - not usable anymore")
sys.exit(1)
elif static.HAMLIB_RADIOCONTROL == "rigctld":
import rigctld as rig
else:
@ -272,20 +244,25 @@ class RF:
)
fft_thread.start()
audio_thread_datac0 = threading.Thread(
target=self.audio_datac0, name="AUDIO_THREAD DATAC0", daemon=True
audio_thread_sig0_datac0 = threading.Thread(
target=self.audio_sig0_datac0, name="AUDIO_THREAD DATAC0 - 0", daemon=True
)
audio_thread_datac0.start()
audio_thread_sig0_datac0.start()
audio_thread_datac1 = threading.Thread(
target=self.audio_datac1, name="AUDIO_THREAD DATAC1", daemon=True
audio_thread_sig1_datac0 = threading.Thread(
target=self.audio_sig1_datac0, name="AUDIO_THREAD DATAC0 - 1", daemon=True
)
audio_thread_datac1.start()
audio_thread_sig1_datac0.start()
audio_thread_datac3 = threading.Thread(
target=self.audio_datac3, name="AUDIO_THREAD DATAC3", daemon=True
audio_thread_dat0_datac1 = threading.Thread(
target=self.audio_dat0_datac1, name="AUDIO_THREAD DATAC1", daemon=True
)
audio_thread_datac3.start()
audio_thread_dat0_datac1.start()
audio_thread_dat0_datac3 = threading.Thread(
target=self.audio_dat0_datac3, name="AUDIO_THREAD DATAC3", daemon=True
)
audio_thread_dat0_datac3.start()
if static.ENABLE_FSK:
audio_thread_fsk_ldpc0 = threading.Thread(
@ -303,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 +305,7 @@ class RF:
depositing the data into the codec data buffers.
"""
while True:
time.sleep(0.01)
threading.Event().wait(0.01)
# -----read
data_in48k = bytes()
with open(RXCHANNEL, "rb") as fifo:
@ -335,15 +319,16 @@ class RF:
length_x = len(x)
for data_buffer, receive in [
(self.datac0_buffer, True),
(self.datac1_buffer, RECEIVE_DATAC1),
(self.datac3_buffer, RECEIVE_DATAC3),
(self.sig0_datac0_buffer, RECEIVE_SIG0),
(self.sig1_datac0_buffer, RECEIVE_SIG1),
(self.dat0_datac1_buffer, RECEIVE_DATAC1),
(self.dat0_datac3_buffer, RECEIVE_DATAC3),
# Not enabled yet.
# (self.fsk_ldpc_buffer_0, static.ENABLE_FSK),
# (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)
@ -351,14 +336,10 @@ class RF:
def mkfifo_write_callback(self) -> None:
"""Support testing by writing the audio data to a pipe."""
while True:
time.sleep(0.01)
threading.Event().wait(0.01)
# -----write
if len(self.modoutqueue) <= 0 or self.mod_out_locked:
# data_out48k = np.zeros(self.AUDIO_FRAMES_PER_BUFFER_RX, dtype=np.int16)
pass
else:
if len(self.modoutqueue) > 0 and not self.mod_out_locked:
data_out48k = self.modoutqueue.popleft()
# print(len(data_out48k))
@ -384,29 +365,44 @@ class RF:
x = np.frombuffer(data_in48k, dtype=np.int16)
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
if not static.TRANSMITTING:
length_x = len(x)
# TODO: Overriding this for testing purposes
# if not static.TRANSMITTING:
length_x = len(x)
# Avoid buffer overflow by filling only if buffer for
# selected datachannel mode is not full
for audiobuffer, receive, index in [
(self.datac0_buffer, True, 0),
(self.datac1_buffer, RECEIVE_DATAC1, 1),
(self.datac3_buffer, RECEIVE_DATAC3, 2),
(self.fsk_ldpc_buffer_0, static.ENABLE_FSK, 3),
(self.fsk_ldpc_buffer_1, static.ENABLE_FSK, 4),
]:
if audiobuffer.nbuffer + length_x > audiobuffer.size:
static.BUFFER_OVERFLOW_COUNTER[index] += 1
elif receive:
audiobuffer.push(x)
# Avoid buffer overflow by filling only if buffer for
# selected datachannel mode is not full
for audiobuffer, receive, index in [
(self.sig0_datac0_buffer, RECEIVE_SIG0, 0),
(self.sig1_datac0_buffer, RECEIVE_SIG1, 1),
(self.dat0_datac1_buffer, RECEIVE_DATAC1, 2),
(self.dat0_datac3_buffer, RECEIVE_DATAC3, 3),
(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:
static.BUFFER_OVERFLOW_COUNTER[index] += 1
elif receive:
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)
self.fft_data = x
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()
self.fft_data = data_out48k
@ -430,18 +426,37 @@ class RF:
frames:
"""
self.log.debug("[MDM] transmit", mode=mode)
"""
sig0 = 14
sig1 = 14
datac0 = 14
datac1 = 10
datac3 = 12
fsk_ldpc = 9
fsk_ldpc_0 = 200
fsk_ldpc_1 = 201
"""
if mode == 14:
freedv = self.freedv_datac0_tx
elif mode == 10:
freedv = self.freedv_datac1_tx
elif mode == 12:
freedv = self.freedv_datac3_tx
else:
return False
static.TRANSMITTING = True
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
static.PTT_STATE = self.hamlib.set_ptt(True)
jsondata = {"ptt": "True"}
data_out = json.dumps(jsondata)
sock.SOCKET_QUEUE.put(data_out)
# static.PTT_STATE = self.hamlib.set_ptt(True)
# jsondata = {"ptt": "True"}
# data_out = json.dumps(jsondata)
# sock.SOCKET_QUEUE.put(data_out)
# Open codec2 instance
self.MODE = mode
freedv = open_codec2_instance(self.MODE)
# Get number of bytes per frame for mode
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8)
@ -466,11 +481,12 @@ class RF:
)
# Add empty data to handle ptt toggle time
data_delay_mseconds = 0 # milliseconds
data_delay = int(self.MODEM_SAMPLE_RATE * (data_delay_mseconds / 1000)) # type: ignore
mod_out_silence = ctypes.create_string_buffer(data_delay * 2)
txbuffer = bytes(mod_out_silence)
#data_delay_mseconds = 0 # milliseconds
#data_delay = int(self.MODEM_SAMPLE_RATE * (data_delay_mseconds / 1000)) # type: ignore
#mod_out_silence = ctypes.create_string_buffer(data_delay * 2)
#txbuffer = bytes(mod_out_silence)
# TODO: Disabled this one for testing
txbuffer = bytes()
self.log.debug(
"[MDM] TRANSMIT", mode=self.MODE, payload=payload_bytes_per_frame
)
@ -534,8 +550,10 @@ class RF:
txbuffer_48k = self.resampler.resample8_to_48(x)
# Explicitly lock our usage of mod_out_queue if needed
# Deactivated for testing purposes
self.mod_out_locked = False
# This could avoid audio problems on slower CPU
# we will fill our modout list with all data, then start
# processing it in audio callback
self.mod_out_locked = True
# -------------------------------
chunk_length = self.AUDIO_FRAMES_PER_BUFFER_TX # 4800
@ -553,11 +571,11 @@ class RF:
self.modoutqueue.append(c)
# Release our mod_out_lock so we can use the queue
# Release our mod_out_lock, so we can use the queue
self.mod_out_locked = False
while self.modoutqueue:
time.sleep(0.01)
threading.Event().wait(0.01)
static.PTT_STATE = self.hamlib.set_ptt(False)
@ -569,7 +587,6 @@ class RF:
# After processing, set the locking state back to true to be prepared for next transmission
self.mod_out_locked = True
self.c_lib.freedv_close(freedv)
self.modem_transmit_queue.task_done()
static.TRANSMITTING = False
threading.Event().set()
@ -585,13 +602,15 @@ class RF:
freedv: ctypes.c_void_p,
bytes_out,
bytes_per_frame,
state_buffer,
mode_name,
) -> int:
"""
De-modulate supplied audio stream with supplied codec2 instance.
Decoded audio is placed into `bytes_out`.
:param buffer: Incoming audio
:type buffer: codec2.audio_buffer
:param audiobuffer: Incoming audio
:type audiobuffer: codec2.audio_buffer
:param nin: Number of frames codec2 is expecting
:type nin: int
:param freedv: codec2 instance
@ -600,56 +619,180 @@ class RF:
:type bytes_out: _type_
:param bytes_per_frame: Number of bytes per frame
: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
"""
nbytes = 0
while self.stream.active:
threading.Event().wait(0.01)
while audiobuffer.nbuffer >= nin:
# demodulate audio
nbytes = codec2.api.freedv_rawdatarx(
freedv, bytes_out, audiobuffer.buffer.ctypes
)
audiobuffer.pop(nin)
nin = codec2.api.freedv_nin(freedv)
if nbytes == bytes_per_frame:
self.log.debug(
"[MDM] [demod_audio] Pushing received data to received_queue"
try:
while self.stream.active:
threading.Event().wait(0.01)
while audiobuffer.nbuffer >= nin:
# demodulate audio
nbytes = codec2.api.freedv_rawdatarx(
freedv, bytes_out, audiobuffer.buffer.ctypes
)
self.modem_received_queue.put([bytes_out, freedv, bytes_per_frame])
# self.get_scatter(freedv)
self.calculate_snr(freedv)
# get current modem states and write to list
# 1 trial
# 2 sync
# 3 trial sync
# 6 decoded
# 10 error decoding == NACK
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)
if nbytes == bytes_per_frame:
# process commands only if static.LISTEN = True
if static.LISTEN:
self.log.debug(
"[MDM] [demod_audio] Pushing received data to received_queue", nbytes=nbytes
)
self.modem_received_queue.put([bytes_out, freedv, bytes_per_frame])
self.get_scatter(freedv)
self.calculate_snr(freedv)
state_buffer = []
else:
self.log.warning(
"[MDM] [demod_audio] received frame but ignored processing",
listen=static.LISTEN
)
except Exception as e:
self.log.warning("[MDM] [demod_audio] Stream not active anymore", e=e)
return nin
def audio_datac0(self) -> None:
"""Receive data encoded with datac0"""
self.datac0_nin = self.demodulate_audio(
self.datac0_buffer,
self.datac0_nin,
self.datac0_freedv,
self.datac0_bytes_out,
self.datac0_bytes_per_frame,
def init_codec2_mode(self, mode, adv):
"""
Init codec2 and return some important parameters
Args:
self:
mode:
adv:
Returns:
c2instance, bytes_per_frame, bytes_out, audio_buffer, nin
"""
if adv:
# FSK Long-distance Parity Code 1 - data frames
c2instance = ctypes.cast(
codec2.api.freedv_open_advanced(
codec2.api.FREEDV_MODE_FSK_LDPC,
ctypes.byref(adv),
),
ctypes.c_void_p,
)
else:
# create codec2 instance
c2instance = ctypes.cast(
codec2.api.freedv_open(mode), ctypes.c_void_p
)
# set tuning range
self.c_lib.freedv_set_tuning_range(
c2instance,
ctypes.c_float(static.TUNING_RANGE_FMIN),
ctypes.c_float(static.TUNING_RANGE_FMAX),
)
def audio_datac1(self) -> None:
# get bytes per frame
bytes_per_frame = int(
codec2.api.freedv_get_bits_per_modem_frame(c2instance) / 8
)
# create byte out buffer
bytes_out = ctypes.create_string_buffer(bytes_per_frame)
# set initial frames per burst
codec2.api.freedv_set_frames_per_burst(c2instance, 1)
# init audio buffer
audio_buffer = codec2.audio_buffer(2 * self.AUDIO_FRAMES_PER_BUFFER_RX)
# get initial nin
nin = codec2.api.freedv_nin(c2instance)
# Additional Datac0-specific information - these are not referenced anywhere else.
# self.sig0_datac0_payload_per_frame = self.sig0_datac0_bytes_per_frame - 2
# self.sig0_datac0_n_nom_modem_samples = self.c_lib.freedv_get_n_nom_modem_samples(
# self.sig0_datac0_freedv
# )
# self.sig0_datac0_n_tx_modem_samples = self.c_lib.freedv_get_n_tx_modem_samples(
# self.sig0_datac0_freedv
# )
# self.sig0_datac0_n_tx_preamble_modem_samples = (
# self.c_lib.freedv_get_n_tx_preamble_modem_samples(self.sig0_datac0_freedv)
# )
# self.sig0_datac0_n_tx_postamble_modem_samples = (
# self.c_lib.freedv_get_n_tx_postamble_modem_samples(self.sig0_datac0_freedv)
# )
# return values
return c2instance, bytes_per_frame, bytes_out, audio_buffer, nin
def audio_sig0_datac0(self) -> None:
"""Receive data encoded with datac0 - 0"""
self.sig0_datac0_nin = self.demodulate_audio(
self.sig0_datac0_buffer,
self.sig0_datac0_nin,
self.sig0_datac0_freedv,
self.sig0_datac0_bytes_out,
self.sig0_datac0_bytes_per_frame,
SIG0_DATAC0_STATE,
"sig0-datac0"
)
def audio_sig1_datac0(self) -> None:
"""Receive data encoded with datac0 - 1"""
self.sig1_datac0_nin = self.demodulate_audio(
self.sig1_datac0_buffer,
self.sig1_datac0_nin,
self.sig1_datac0_freedv,
self.sig1_datac0_bytes_out,
self.sig1_datac0_bytes_per_frame,
SIG1_DATAC0_STATE,
"sig1-datac0"
)
def audio_dat0_datac1(self) -> None:
"""Receive data encoded with datac1"""
self.datac1_nin = self.demodulate_audio(
self.datac1_buffer,
self.datac1_nin,
self.datac1_freedv,
self.datac1_bytes_out,
self.datac1_bytes_per_frame,
self.dat0_datac1_nin = self.demodulate_audio(
self.dat0_datac1_buffer,
self.dat0_datac1_nin,
self.dat0_datac1_freedv,
self.dat0_datac1_bytes_out,
self.dat0_datac1_bytes_per_frame,
DAT0_DATAC1_STATE,
"dat0-datac1"
)
def audio_datac3(self) -> None:
def audio_dat0_datac3(self) -> None:
"""Receive data encoded with datac3"""
self.datac3_nin = self.demodulate_audio(
self.datac3_buffer,
self.datac3_nin,
self.datac3_freedv,
self.datac3_bytes_out,
self.datac3_bytes_per_frame,
self.dat0_datac3_nin = self.demodulate_audio(
self.dat0_datac3_buffer,
self.dat0_datac3_nin,
self.dat0_datac3_freedv,
self.dat0_datac3_bytes_out,
self.dat0_datac3_bytes_per_frame,
DAT0_DATAC3_STATE,
"dat0-datac3"
)
def audio_fsk_ldpc_0(self) -> None:
@ -675,9 +818,14 @@ class RF:
def worker_transmit(self) -> None:
"""Worker for FIFO queue for processing frames to be transmitted"""
while True:
# print queue size for debugging purposes
# TODO: Lets check why we have several frames in our transmit queue which causes sometimes a double transmission
# we could do a cleanup after a transmission so theres no reason sending twice
queuesize = self.modem_transmit_queue.qsize()
self.log.debug("[MDM] self.modem_transmit_queue", qsize=queuesize)
data = self.modem_transmit_queue.get()
self.log.debug("[MDM] worker_transmit", mode=data[0])
# self.log.debug("[MDM] worker_transmit", mode=data[0])
self.transmit(
mode=data[0], repeats=data[1], repeat_delay=data[2], frames=data[3]
)
@ -705,7 +853,6 @@ class RF:
:rtype: float
"""
modemStats = codec2.MODEMSTATS()
self.c_lib.freedv_get_modem_extended_stats.restype = None
self.c_lib.freedv_get_modem_extended_stats(freedv, ctypes.byref(modemStats))
offset = round(modemStats.foff) * (-1)
static.FREQ_OFFSET = offset
@ -723,28 +870,34 @@ class RF:
return
modemStats = codec2.MODEMSTATS()
self.c_lib.freedv_get_modem_extended_stats.restype = None
self.c_lib.freedv_get_modem_extended_stats(freedv, ctypes.byref(modemStats))
ctypes.cast(
self.c_lib.freedv_get_modem_extended_stats(freedv, ctypes.byref(modemStats)),
ctypes.c_void_p,
)
scatterdata = []
scatterdata_small = []
for i in range(codec2.MODEM_STATS_NC_MAX):
for j in range(codec2.MODEM_STATS_NR_MAX):
# check if odd or not to get every 2nd item for x
if (j % 2) == 0:
xsymbols = round(modemStats.rx_symbols[i][j] / 1000)
ysymbols = round(modemStats.rx_symbols[i][j + 1] / 1000)
# check if value 0.0 or has real data
if xsymbols != 0.0 and ysymbols != 0.0:
scatterdata.append({"x": xsymbols, "y": ysymbols})
# original function before itertool
#for i in range(codec2.MODEM_STATS_NC_MAX):
# for j in 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)})
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
if 150 > len(scatterdata) > 0:
static.SCATTER = scatterdata
else:
# only take every tenth data point
scatterdata_small = scatterdata[::10]
static.SCATTER = scatterdata_small
static.SCATTER = scatterdata[::10]
def calculate_snr(self, freedv: ctypes.c_void_p) -> float:
"""
@ -769,16 +922,30 @@ class RF:
snr = round(modem_stats_snr, 1)
self.log.info("[MDM] calculate_snr: ", snr=snr)
# static.SNR = np.clip(snr, 0, 255) # limit to max value of 255
static.SNR = np.clip(
snr, -128, 128
) # limit to max value of -128/128 as a possible fix of #188
static.SNR = snr
#static.SNR = np.clip(
# snr, -127, 127
#) # limit to max value of -128/128 as a possible fix of #188
return static.SNR
except Exception as err:
self.log.error(f"[MDM] calculate_snr: Exception: {err}")
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
@ -788,10 +955,11 @@ class RF:
- static.HAMLIB_BANDWIDTH
"""
while True:
threading.Event().wait(0.5)
threading.Event().wait(0.25)
static.HAMLIB_FREQUENCY = self.hamlib.get_frequency()
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:
"""
@ -801,8 +969,11 @@ class RF:
# Initialize channel_busy_delay counter
channel_busy_delay = 0
# Initialize dbfs counter
rms_counter = 0
while True:
# time.sleep(0.01)
# threading.Event().wait(0.01)
threading.Event().wait(0.01)
# WE NEED TO OPTIMIZE THIS!
@ -828,19 +999,60 @@ class RF:
# Have to do this when we are not transmitting so our
# own sending data will not affect this too much
if not static.TRANSMITTING:
dfft[dfft > avg + 10] = 100
dfft[dfft > avg + 15] = 100
# Calculate audio max value
# static.AUDIO_RMS = np.amax(self.fft_data)
# Calculate audio dbfs
# https://stackoverflow.com/a/9763652
# calculate dbfs every 50 cycles for reducing CPU load
rms_counter += 1
if rms_counter > 50:
d = np.frombuffer(self.fft_data, np.int16).astype(np.float32)
# calculate RMS and then dBFS
# TODO: Need to change static.AUDIO_RMS to AUDIO_DBFS somewhen
# https://dsp.stackexchange.com/questions/8785/how-to-compute-dbfs
# 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(
"[MDM] fft calculation error - please check your audio setup",
e=e,
)
static.AUDIO_DBFS = -100
rms_counter = 0
# Convert data to int to decrease size
dfft = dfft.astype(int)
# Create list of dfft for later pushing to static.FFT
dfftlist = dfft.tolist()
# Reduce area where the busy detection is enabled
# We want to have this in correlation with mode bandwidth
# TODO: This is not correctly and needs to be checked for correct maths
# dfftlist[0:1] = 10,15Hz
# Bandwidth[Hz] / 10,15
# narrowband = 563Hz = 56
# wideband = 1700Hz = 167
# 1500Hz = 148
# 2700Hz = 266
# 3200Hz = 315
# define the area, we are detecting busy state
dfft = dfft[120:176] if static.LOW_BANDWIDTH_MODE else dfft[65:231]
# Check for signals higher than average by checking for "100"
# If we have a signal, increment our channel_busy delay counter
# so we have a smoother state toggle
if np.sum(dfft[dfft > avg + 10]) >= 300 and not static.TRANSMITTING:
if np.sum(dfft[dfft > avg + 15]) >= 400 and not static.TRANSMITTING:
static.CHANNEL_BUSY = True
# Limit delay counter to a maximun of 50. 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 + 5, 50)
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)
@ -848,11 +1060,7 @@ class RF:
if channel_busy_delay == 0:
static.CHANNEL_BUSY = False
# Round data to decrease size
dfft = np.around(dfft, 0)
dfftlist = dfft.tolist()
static.FFT = dfftlist[:320] # 320 --> bandwidth 3000
static.FFT = dfftlist[:315] # 315 --> bandwidth 3200
except Exception as err:
self.log.error(f"[MDM] calculate_fft: Exception: {err}")
self.log.debug("[MDM] Setting fft=0")
@ -870,8 +1078,8 @@ class RF:
frames_per_burst = min(frames_per_burst, 1)
frames_per_burst = max(frames_per_burst, 5)
codec2.api.freedv_set_frames_per_burst(self.datac1_freedv, frames_per_burst)
codec2.api.freedv_set_frames_per_burst(self.datac3_freedv, frames_per_burst)
codec2.api.freedv_set_frames_per_burst(self.dat0_datac1_freedv, frames_per_burst)
codec2.api.freedv_set_frames_per_burst(self.dat0_datac3_freedv, frames_per_burst)
codec2.api.freedv_set_frames_per_burst(self.fsk_ldpc_freedv_0, frames_per_burst)
@ -932,8 +1140,31 @@ def set_audio_volume(datalist, volume: float) -> np.int16:
:return: Scaled audio samples
:rtype: np.int16
"""
# make sure we have float as data type to avoid crash
try:
volume = float(volume)
except Exception as e:
print(f"[MDM] changing audio volume failed with error: {e}")
volume = 100.0
# Clip volume provided to acceptable values
volume = np.clip(volume, 0, 200) # limit to max value of 255
# Scale samples by the ratio of volume / 100.0
data = np.fromstring(datalist, np.int16) * (volume / 100.0) # type: ignore
return data.astype(np.int16)
def get_modem_error_state():
"""
get current state buffer and return True of contains 10
"""
if RECEIVE_DATAC1 and 10 in DAT0_DATAC1_STATE:
DAT0_DATAC1_STATE.clear()
return True
if RECEIVE_DATAC3 and 10 in DAT0_DATAC3_STATE:
DAT0_DATAC3_STATE.clear()
return True
return False

View file

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

View file

@ -1,278 +0,0 @@
import atexit
import os
import re
import subprocess
import sys
import structlog
mainlog = structlog.get_logger("rig")
# set global hamlib version
hamlib_version = 0
# append local search path
# check if we are running in a pyinstaller environment
if hasattr(sys, "_MEIPASS"):
sys.path.append(getattr(sys, "_MEIPASS"))
else:
sys.path.append(os.path.abspath("."))
# try importing hamlib
try:
# get python version
python_version = f"{str(sys.version_info[0])}.{str(sys.version_info[1])}"
# installation path for Ubuntu 20.04 LTS python modules
# sys.path.append(f"/usr/local/lib/python{python_version}/site-packages")
# installation path for Ubuntu 20.10 +
sys.path.append("/usr/local/lib/")
# installation path for Suse
sys.path.append(f"/usr/local/lib64/python{python_version}/site-packages")
# everything else... not nice, but an attempt to see how it's running within app bundle
# this is not needed as python will be shipped with app bundle
sys.path.append("/usr/local/lib/python3.6/site-packages")
sys.path.append("/usr/local/lib/python3.7/site-packages")
sys.path.append("/usr/local/lib/python3.8/site-packages")
sys.path.append("/usr/local/lib/python3.9/site-packages")
sys.path.append("/usr/local/lib/python3.10/site-packages")
sys.path.append("lib/hamlib/linux/python3.8/site-packages")
import Hamlib
# https://stackoverflow.com/a/4703409
hamlib_version = re.findall(r"[-+]?\d*\.?\d+|\d+", Hamlib.cvar.hamlib_version)
hamlib_version = float(hamlib_version[0])
min_hamlib_version = 4.1
if hamlib_version > min_hamlib_version:
mainlog.info("[RIG] Hamlib found", version=hamlib_version)
else:
mainlog.warning(
"[RIG] Hamlib outdated", found=hamlib_version, recommend=min_hamlib_version
)
except Exception as err:
mainlog.warning("[RIG] Python Hamlib binding not found", error=err)
try:
mainlog.warning("[RIG] Trying to open rigctl")
rigctl = subprocess.Popen(
"rigctl -V",
shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True,
)
hamlib_version = rigctl.stdout.readline()
hamlib_version = hamlib_version.split(" ")
if hamlib_version[1] == "Hamlib":
mainlog.warning(
"[RIG] Rigctl found! Please try using this", version=hamlib_version[2]
)
sys.exit()
else:
raise Exception
except Exception as err1:
mainlog.critical("[RIG] HAMLIB NOT INSTALLED", error=err1)
hamlib_version = 0
sys.exit()
class radio:
""" """
log = structlog.get_logger(__name__)
def __init__(self):
self.devicename = ""
self.devicenumber = ""
self.deviceport = ""
self.serialspeed = ""
self.hamlib_ptt_type = ""
self.my_rig = ""
self.pttport = ""
self.data_bits = ""
self.stop_bits = ""
self.handshake = ""
def open_rig(
self,
devicename,
deviceport,
hamlib_ptt_type,
serialspeed,
pttport,
data_bits,
stop_bits,
handshake,
rigctld_port,
rigctld_ip,
):
"""
Args:
devicename:
deviceport:
hamlib_ptt_type:
serialspeed:
pttport:
data_bits:
stop_bits:
handshake:
rigctld_port:
rigctld_ip:
"""
self.devicename = devicename
self.deviceport = str(deviceport)
# we need to ensure this is a str, otherwise set_conf functions are crashing
self.serialspeed = str(serialspeed)
self.hamlib_ptt_type = str(hamlib_ptt_type)
self.pttport = str(pttport)
self.data_bits = str(data_bits)
self.stop_bits = str(stop_bits)
self.handshake = str(handshake)
# try to init hamlib
try:
Hamlib.rig_set_debug(Hamlib.RIG_DEBUG_NONE)
# get devicenumber by looking for deviceobject in Hamlib module
try:
self.devicenumber = int(getattr(Hamlib, self.devicename))
except Exception:
self.log.error("[RIG] Hamlib: rig not supported...")
self.devicenumber = 0
self.my_rig = Hamlib.Rig(self.devicenumber)
self.my_rig.set_conf("rig_pathname", self.deviceport)
self.my_rig.set_conf("retry", "5")
self.my_rig.set_conf("serial_speed", self.serialspeed)
self.my_rig.set_conf("serial_handshake", self.handshake)
self.my_rig.set_conf("stop_bits", self.stop_bits)
self.my_rig.set_conf("data_bits", self.data_bits)
self.my_rig.set_conf("ptt_pathname", self.pttport)
if self.hamlib_ptt_type == "RIG":
self.hamlib_ptt_type = Hamlib.RIG_PTT_RIG
self.my_rig.set_conf("ptt_type", "RIG")
elif self.hamlib_ptt_type == "USB":
self.hamlib_ptt_type = Hamlib.RIG_PORT_USB
self.my_rig.set_conf("ptt_type", "USB")
elif self.hamlib_ptt_type == "DTR-H":
self.hamlib_ptt_type = Hamlib.RIG_PTT_SERIAL_DTR
self.my_rig.set_conf("dtr_state", "HIGH")
self.my_rig.set_conf("ptt_type", "DTR")
elif self.hamlib_ptt_type == "DTR-L":
self.hamlib_ptt_type = Hamlib.RIG_PTT_SERIAL_DTR
self.my_rig.set_conf("dtr_state", "LOW")
self.my_rig.set_conf("ptt_type", "DTR")
elif self.hamlib_ptt_type == "RTS":
self.hamlib_ptt_type = Hamlib.RIG_PTT_SERIAL_RTS
self.my_rig.set_conf("dtr_state", "OFF")
self.my_rig.set_conf("ptt_type", "RTS")
elif self.hamlib_ptt_type == "PARALLEL":
self.hamlib_ptt_type = Hamlib.RIG_PTT_PARALLEL
elif self.hamlib_ptt_type == "MICDATA":
self.hamlib_ptt_type = Hamlib.RIG_PTT_RIG_MICDATA
elif self.hamlib_ptt_type == "CM108":
self.hamlib_ptt_type = Hamlib.RIG_PTT_CM108
elif self.hamlib_ptt_type == "RIG_PTT_NONE":
self.hamlib_ptt_type = Hamlib.RIG_PTT_NONE
else: # self.hamlib_ptt_type == "RIG_PTT_NONE":
self.hamlib_ptt_type = Hamlib.RIG_PTT_NONE
self.log.info(
"[RIG] Opening...",
device=self.devicenumber,
path=self.my_rig.get_conf("rig_pathname"),
serial_speed=self.my_rig.get_conf("serial_speed"),
serial_handshake=self.my_rig.get_conf("serial_handshake"),
stop_bits=self.my_rig.get_conf("stop_bits"),
data_bits=self.my_rig.get_conf("data_bits"),
ptt_pathname=self.my_rig.get_conf("ptt_pathname"),
)
self.my_rig.open()
atexit.register(self.my_rig.close)
try:
# lets determine the error message when opening rig
error = str(Hamlib.rigerror(self.my_rig.error_status)).splitlines()
error = error[1].split("err=")
error = error[1]
if error == "Permission denied":
self.log.error("[RIG] Hamlib has no permissions", e=error)
help_url = "https://github.com/DJ2LS/FreeDATA/wiki/UBUNTU-Manual-installation#1-permissions"
self.log.error("[RIG] HELP:", check=help_url)
except Exception:
self.log.info("[RIG] Hamlib device opened", status="SUCCESS")
# set ptt to false if ptt is stuck for some reason
self.set_ptt(False)
# set rig mode to USB
# temporarly outcommented because of possible problems.
# self.my_rig.set_mode(Hamlib.RIG_MODE_USB)
# self.my_rig.set_mode(Hamlib.RIG_MODE_PKTUSB)
return True
except Exception as err2:
self.log.error(
"[RIG] Hamlib - can't open rig", error=err2, e=sys.exc_info()[0]
)
return False
def get_frequency(self):
""" """
return int(self.my_rig.get_freq())
def get_mode(self):
""" """
(hamlib_mode, bandwidth) = self.my_rig.get_mode()
return Hamlib.rig_strrmode(hamlib_mode)
def get_bandwidth(self):
""" """
(hamlib_mode, bandwidth) = self.my_rig.get_mode()
return bandwidth
# not needed yet beacuse of some possible problems
# def set_mode(self, mode):
# return 0
def get_ptt(self):
""" """
return self.my_rig.get_ptt()
def set_ptt(self, state):
"""
Args:
state:
Returns:
"""
if state:
self.my_rig.set_ptt(Hamlib.RIG_VFO_CURR, 1)
else:
self.my_rig.set_ptt(Hamlib.RIG_VFO_CURR, 0)
return state
def close_rig(self):
""" """
self.my_rig.close()

View file

@ -1,214 +0,0 @@
# Intially created by Franco Spinelli, IW2DHW, 01/2022
# Updated by DJ2LS
#
# versione mia di rig.py per gestire Ft897D tramite rigctl e senza
# fare alcun riferimento alla configurazione
#
# e' una pezza clamorosa ma serve per poter provare on-air il modem
#
import os
import subprocess
import sys
import time
import structlog
# for rig_model -> rig_number only
# set global hamlib version
hamlib_version = 0
class radio:
""" """
log = structlog.get_logger("radio (rigctl)")
def __init__(self):
self.devicename = ""
self.devicenumber = ""
self.deviceport = ""
self.serialspeed = ""
self.hamlib_ptt_type = ""
self.my_rig = ""
self.pttport = ""
self.data_bits = ""
self.stop_bits = ""
self.handshake = ""
self.cmd = ""
def open_rig(
self,
devicename,
deviceport,
hamlib_ptt_type,
serialspeed,
pttport,
data_bits,
stop_bits,
handshake,
rigctld_ip,
rigctld_port,
):
"""
Args:
devicename:
deviceport:
hamlib_ptt_type:
serialspeed:
pttport:
data_bits:
stop_bits:
handshake:
rigctld_ip:
rigctld_port:
Returns:
"""
self.devicename = devicename
self.deviceport = deviceport
# we need to ensure this is a str, otherwise set_conf functions are crashing
self.serialspeed = str(serialspeed)
self.hamlib_ptt_type = hamlib_ptt_type
self.pttport = pttport
self.data_bits = data_bits
self.stop_bits = stop_bits
self.handshake = handshake
# check if we are running in a pyinstaller environment
if hasattr(sys, "_MEIPASS"):
sys.path.append(getattr(sys, "_MEIPASS"))
else:
sys.path.append(os.path.abspath("."))
# get devicenumber by looking for deviceobject in Hamlib module
try:
import Hamlib
self.devicenumber = int(getattr(Hamlib, self.devicename))
except Exception as err:
if int(self.devicename):
self.devicenumber = int(self.devicename)
else:
self.devicenumber = 6 # dummy
self.log.warning("[RIGCTL] Radio not found. Using DUMMY!", error=err)
# set deviceport to dummy port, if we selected dummy model
if self.devicenumber in {1, 6}:
self.deviceport = "/dev/ttyUSB0"
print(self.devicenumber, self.deviceport, self.serialspeed)
# select precompiled executable for win32/win64 rigctl
# this is really a hack...somewhen we need a native hamlib integration for windows
if sys.platform in ["win32", "win64"]:
self.cmd = (
app_path
+ "lib\\hamlib\\"
+ sys.platform
+ (
f"\\rigctl -m {self.devicenumber} "
f"-r {self.deviceport} "
f"-s {int(self.serialspeed)} "
)
)
else:
self.cmd = "rigctl -m %d -r %s -s %d " % (
self.devicenumber,
self.deviceport,
int(self.serialspeed),
)
# eseguo semplicemente rigctl con il solo comando T 1 o T 0 per
# il set e t per il get
# set ptt to false if ptt is stuck for some reason
self.set_ptt(False)
return True
def get_frequency(self):
""" """
cmd = f"{self.cmd} f"
sw_proc = subprocess.Popen(
cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True
)
time.sleep(0.5)
freq = sw_proc.communicate()[0]
# print("get_frequency", freq, sw_proc.communicate())
try:
return int(freq)
except Exception:
return False
def get_mode(self):
""" """
# (hamlib_mode, bandwidth) = self.my_rig.get_mode()
# return Hamlib.rig_strrmode(hamlib_mode)
try:
return "PKTUSB"
except Exception:
return False
def get_bandwidth(self):
""" """
# (hamlib_mode, bandwidth) = self.my_rig.get_mode()
bandwidth = 2700
try:
return bandwidth
except Exception:
return False
def set_mode(self, mode):
"""
Args:
mode:
Returns:
"""
# non usata
return 0
def get_ptt(self):
""" """
cmd = f"{self.cmd} t"
sw_proc = subprocess.Popen(
cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True
)
time.sleep(0.5)
status = sw_proc.communicate()[0]
try:
return status
except Exception:
return False
def set_ptt(self, state):
"""
Args:
state:
Returns:
"""
cmd = f"{self.cmd} T "
print("set_ptt", state)
cmd = f"{cmd}1" if state else f"{cmd}0"
print("set_ptt", cmd)
sw_proc = subprocess.Popen(cmd, shell=True, text=True)
try:
return state
except Exception:
return False
def close_rig(self):
""" """
# self.my_rig.close()
return

View file

@ -1,13 +1,14 @@
#!/usr/bin/env python3
# class taken from darsidelemm
# class taken from darksidelemm
# rigctl - https://github.com/darksidelemm/rotctld-web-gui/blob/master/rotatorgui.py#L35
#
# modified and adjusted to FreeDATA needs by DJ2LS
import contextlib
import socket
import time
import structlog
import threading
# set global hamlib version
hamlib_version = 0
@ -16,20 +17,24 @@ hamlib_version = 0
class radio:
"""rigctld (hamlib) communication class"""
# Note: This is a massive hack.
log = structlog.get_logger("radio (rigctld)")
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.sock.settimeout(timeout)
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
# class wide variable for some parameters
self.bandwidth = ''
self.frequency = ''
self.mode = ''
def open_rig(
self,
devicename,
@ -63,42 +68,79 @@ 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(
"[RIGCTLD] Can't connect to rigctld!", ip=self.hostname, port=self.port
"[RIGCTLD] Can't connect!", ip=self.hostname, port=self.port
)
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] Connection to rigctld refused! 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) -> bytes:
def send_ptt_command(self, command, expect_answer) -> bytes:
"""Send a command to the connected rotctld instance,
and return the return value.
@ -106,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!",
@ -116,10 +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:
return self.connection.recv(1024)
# 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.data_connection.recv(64) if expect_answer else True
except Exception:
self.log.warning(
"[RIGCTLD] No command response!",
@ -127,47 +192,62 @@ class radio:
ip=self.hostname,
port=self.port,
)
self.connected = False
else:
# reconnecting....
time.sleep(0.5)
self.connect()
self.data_connected = False
return b""
def get_status(self):
""" """
return "connected" if self.data_connected and self.ptt_connected else "unknown/disconnected"
def get_mode(self):
""" """
try:
data = self.send_command(b"m")
data = self.send_data_command(b"m", True)
data = data.split(b"\n")
mode = data[0]
return mode.decode("utf-8")
data = data[0].decode("utf-8")
if 'RPRT' not in data:
try:
data = int(data)
except ValueError:
self.mode = str(data)
return self.mode
except Exception:
return 0
return self.mode
def get_bandwidth(self):
""" """
try:
data = self.send_command(b"m")
data = self.send_data_command(b"m", True)
data = data.split(b"\n")
bandwidth = data[1]
return bandwidth.decode("utf-8")
data = data[1].decode("utf-8")
if 'RPRT' not in data and data not in ['']:
with contextlib.suppress(ValueError):
self.bandwidth = int(data)
return self.bandwidth
except Exception:
return 0
return self.bandwidth
def get_frequency(self):
""" """
try:
frequency = self.send_command(b"f")
return frequency.decode("utf-8")
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):
data = int(data)
# make sure we have a frequency and not bandwidth
if data >= 10000:
self.frequency = data
return self.frequency
except Exception:
return 0
return self.frequency
def get_ptt(self):
""" """
try:
return self.send_command(b"t")
return self.send_command(b"t", True)
except Exception:
return False
@ -182,9 +262,39 @@ class radio:
"""
try:
if state:
self.send_command(b"T 1")
self.send_ptt_command(b"T 1", False)
else:
self.send_command(b"T 0")
self.send_ptt_command(b"T 0", False)
return state
except Exception:
return False
def set_frequency(self, frequency):
"""
Args:
frequency:
Returns:
"""
try:
command = bytes(f"F {frequency}", "utf-8")
self.send_data_command(command, False)
except Exception:
return False
def set_mode(self, mode):
"""
Args:
mode:
Returns:
"""
try:
command = bytes(f"M {mode} {self.bandwidth}", "utf-8")
self.send_data_command(command, False)
except Exception:
return False

View file

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

74
tnc/selftest.py Normal file
View file

@ -0,0 +1,74 @@
"""
simple TNC self tests
"""
# -*- coding: utf-8 -*-
# pylint: disable=invalid-name, line-too-long, c-extension-no-member
# pylint: disable=import-outside-toplevel, attribute-defined-outside-init
import sys
import structlog
log = structlog.get_logger("selftest")
class TEST():
def __init__(self):
log.info("[selftest] running self tests...")
if self.run_tests():
log.info("[selftest] passed -> starting TNC")
else:
log.error("[selftest] failed -> closing TNC")
sys.exit(0)
def run_tests(self):
return bool(
self.check_imports()
and self.check_sounddevice()
and self.check_helpers()
)
def check_imports(self):
try:
import argparse
import atexit
import multiprocessing
import os
import signal
import socketserver
import sys
import threading
import time
import structlog
import crcengine
import ctypes
import glob
import enum
import numpy
import sounddevice
return True
except Exception as e:
log.info("[selftest] [check_imports] [failed]", e=e)
return False
def check_sounddevice(self):
try:
import audio
audio.get_audio_devices()
return True
except Exception as e:
log.info("[selftest] [check_sounddevice] [failed]", e=e)
return False
def check_helpers(self):
try:
import helpers
valid_crc24 = "f86ed0"
if helpers.get_crc_24(b"test").hex() == valid_crc24:
return True
else:
raise Exception
except Exception as e:
log.info("[selftest] [check_helpers] [failed]", e=e)
return False

View file

@ -24,13 +24,14 @@ import socketserver
import sys
import threading
import time
import wave
import helpers
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()
@ -76,7 +77,7 @@ class ThreadedTCPRequestHandler(socketserver.StreamRequestHandler):
if data != tempdata:
tempdata = data
SOCKET_QUEUE.put(data)
time.sleep(0.5)
threading.Event().wait(0.5)
while not SOCKET_QUEUE.empty():
data = SOCKET_QUEUE.get()
@ -84,20 +85,23 @@ class ThreadedTCPRequestHandler(socketserver.StreamRequestHandler):
sock_data += b"\n" # append line limiter
# send data to all clients
# try:
for client in CONNECTED_CLIENTS:
try:
client.send(sock_data)
except Exception as err:
self.log.info("[SCK] Connection lost", e=err)
self.connection_alive = False
try:
for client in CONNECTED_CLIENTS:
try:
client.send(sock_data)
except Exception as err:
self.log.info("[SCK] Connection lost", e=err)
# TODO: Check if we really should set connection alive to false. This might disconnect all other clients as well...
self.connection_alive = False
except Exception as err:
self.log.debug("[SCK] catch harmless RuntimeError: Set changed size during iteration", e=err)
# we want to transmit scatter data only once to reduce network traffic
static.SCATTER = []
# we want to display INFO messages only once
static.INFO = []
# self.request.sendall(sock_data)
time.sleep(0.15)
threading.Event().wait(0.15)
def receive_from_client(self):
"""
@ -132,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
time.sleep(3)
threading.Event().wait(0.5)
# finally delete our rx buffer to be ready for new commands
data = bytes()
@ -169,7 +173,7 @@ class ThreadedTCPRequestHandler(socketserver.StreamRequestHandler):
# keep connection alive until we close it
while self.connection_alive and not CLOSE_SIGNAL:
time.sleep(1)
threading.Event().wait(1)
def finish(self):
""" """
@ -204,6 +208,72 @@ def process_tnc_commands(data):
# convert data to json object
received_json = json.loads(data)
log.debug("[SCK] CMD", command=received_json)
# ENABLE TNC LISTENING STATE -----------------------------------------------------
if received_json["type"] == "set" and received_json["command"] == "listen":
try:
static.LISTEN = received_json["state"] in ['true', 'True', True, "ON", "on"]
command_response("listen", True)
# if tnc is connected, force disconnect when static.LISTEN == False
if not static.LISTEN and static.ARQ_SESSION_STATE not in ["disconnecting", "disconnected", "failed"]:
DATA_QUEUE_TRANSMIT.put(["DISCONNECT"])
# set early disconnecting state so we can interrupt connection attempts
static.ARQ_SESSION_STATE = "disconnecting"
command_response("disconnect", True)
except Exception as err:
command_response("listen", False)
log.warning(
"[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 -----------------------------------------------------
if received_json["type"] == "set" and received_json["command"] == "respond_to_call":
try:
static.RESPOND_TO_CALL = received_json["state"] in ['true', 'True', True]
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 RESPOND TO CQ -----------------------------------------------------
if received_json["type"] == "set" and received_json["command"] == "respond_to_cq":
try:
static.RESPOND_TO_CQ = received_json["state"] in ['true', 'True', True]
command_response("respond_to_cq", True)
except Exception as err:
command_response("respond_to_cq", False)
log.warning(
"[SCK] CQ command execution error", e=err, command=received_json
)
# SET TX AUDIO LEVEL -----------------------------------------------------
if (
received_json["type"] == "set"
@ -287,13 +357,22 @@ def process_tnc_commands(data):
if not str(dxcallsign).strip():
raise NoCallsign
# additional step for beeing sure our callsign is correctly
# additional step for being sure our callsign is correctly
# in case we are not getting a station ssid
# then we are forcing a station ssid = 0
dxcallsign = helpers.callsign_to_bytes(dxcallsign)
dxcallsign = helpers.bytes_to_callsign(dxcallsign)
DATA_QUEUE_TRANSMIT.put(["PING", dxcallsign])
# check if specific callsign is set with different SSID than the TNC is initialized
try:
mycallsign = received_json["mycallsign"]
mycallsign = helpers.callsign_to_bytes(mycallsign)
mycallsign = helpers.bytes_to_callsign(mycallsign)
except Exception:
mycallsign = static.MYCALLSIGN
DATA_QUEUE_TRANSMIT.put(["PING", mycallsign, dxcallsign])
command_response("ping", True)
except NoCallsign:
command_response("ping", False)
@ -306,36 +385,79 @@ def process_tnc_commands(data):
# CONNECT ----------------------------------------------------------
if received_json["type"] == "arq" and received_json["command"] == "connect":
# pause our beacon first
static.BEACON_PAUSE = True
# send ping frame and wait for ACK
# check for connection attempts key
try:
dxcallsign = received_json["dxcallsign"]
attempts = int(received_json["attempts"])
except Exception:
# 15 == self.session_connect_max_retries
attempts = 15
# additional step for beeing sure our callsign is correctly
# in case we are not getting a station ssid
# then we are forcing a station ssid = 0
dxcallsign = helpers.callsign_to_bytes(dxcallsign)
dxcallsign = helpers.bytes_to_callsign(dxcallsign)
dxcallsign = received_json["dxcallsign"]
static.DXCALLSIGN = dxcallsign
static.DXCALLSIGN_CRC = helpers.get_crc_24(static.DXCALLSIGN)
# check if specific callsign is set with different SSID than the TNC is initialized
try:
mycallsign = received_json["mycallsign"]
mycallsign = helpers.callsign_to_bytes(mycallsign)
mycallsign = helpers.bytes_to_callsign(mycallsign)
DATA_QUEUE_TRANSMIT.put(["CONNECT", dxcallsign])
command_response("connect", True)
except Exception as err:
except Exception:
mycallsign = static.MYCALLSIGN
# additional step for being sure our callsign is correctly
# in case we are not getting a station ssid
# then we are forcing a station ssid = 0
dxcallsign = helpers.callsign_to_bytes(dxcallsign)
dxcallsign = helpers.bytes_to_callsign(dxcallsign)
if static.ARQ_SESSION_STATE not in ["disconnected", "failed"]:
command_response("connect", False)
log.warning(
"[SCK] Connect command execution error",
e=err,
e=f"already connected to station:{static.DXCALLSIGN}",
command=received_json,
)
else:
# finally check again if we are disconnected or failed
# try connecting
try:
DATA_QUEUE_TRANSMIT.put(["CONNECT", mycallsign, dxcallsign, attempts])
command_response("connect", True)
except Exception as err:
command_response("connect", False)
log.warning(
"[SCK] Connect command execution error",
e=err,
command=received_json,
)
# allow beacon transmission again
static.BEACON_PAUSE = False
# allow beacon transmission again
static.BEACON_PAUSE = False
# DISCONNECT ----------------------------------------------------------
if received_json["type"] == "arq" and received_json["command"] == "disconnect":
# send ping frame and wait for ACK
try:
DATA_QUEUE_TRANSMIT.put(["DISCONNECT"])
command_response("disconnect", True)
if static.ARQ_SESSION_STATE not in ["disconnecting", "disconnected", "failed"]:
DATA_QUEUE_TRANSMIT.put(["DISCONNECT"])
# set early disconnecting state so we can interrupt connection attempts
static.ARQ_SESSION_STATE = "disconnecting"
command_response("disconnect", True)
else:
command_response("disconnect", False)
log.warning(
"[SCK] Disconnect command not possible",
state=static.ARQ_SESSION_STATE,
command=received_json,
)
except Exception as err:
command_response("disconnect", False)
log.warning(
@ -347,6 +469,7 @@ def process_tnc_commands(data):
# TRANSMIT RAW DATA -------------------------------------------
if received_json["type"] == "arq" and received_json["command"] == "send_raw":
static.BEACON_PAUSE = True
try:
if not static.ARQ_SESSION:
dxcallsign = received_json["parameter"][0]["dxcallsign"]
@ -369,9 +492,20 @@ def process_tnc_commands(data):
# check if specific callsign is set with different SSID than the TNC is initialized
try:
mycallsign = received_json["parameter"][0]["mycallsign"]
mycallsign = helpers.callsign_to_bytes(mycallsign)
mycallsign = helpers.bytes_to_callsign(mycallsign)
except Exception:
mycallsign = static.MYCALLSIGN
# check for connection attempts key
try:
attempts = int(received_json["parameter"][0]["attempts"])
except Exception:
# 15 == self.session_connect_max_retries
attempts = 15
# check if transmission uuid provided else set no-uuid
try:
arq_uuid = received_json["uuid"]
@ -384,7 +518,7 @@ def process_tnc_commands(data):
binarydata = base64.b64decode(base64data)
DATA_QUEUE_TRANSMIT.put(
["ARQ_RAW", binarydata, mode, n_frames, arq_uuid, mycallsign]
["ARQ_RAW", binarydata, mode, n_frames, arq_uuid, mycallsign, dxcallsign, attempts]
)
except Exception as err:
@ -460,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)
@ -478,7 +638,7 @@ def send_tnc_state():
"arq_state": str(static.ARQ_STATE),
"arq_session": str(static.ARQ_SESSION),
"arq_session_state": str(static.ARQ_SESSION_STATE),
"audio_rms": str(static.AUDIO_RMS),
"audio_dbfs": str(static.AUDIO_DBFS),
"snr": str(static.SNR),
"frequency": str(static.HAMLIB_FREQUENCY),
"speed_level": str(static.ARQ_SPEED_LEVEL),
@ -491,14 +651,20 @@ 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,
"listen": str(static.LISTEN),
"audio_recording": str(static.AUDIO_RECORD),
}
# add heard stations to heard stations object
@ -514,7 +680,6 @@ def send_tnc_state():
"frequency": heard[6],
}
)
return json.dumps(output)
@ -608,6 +773,17 @@ def process_daemon_commands(data):
tx_audio_level = str(received_json["parameter"][0]["tx_audio_level"])
respond_to_cq = str(received_json["parameter"][0]["respond_to_cq"])
rx_buffer_size = str(received_json["parameter"][0]["rx_buffer_size"])
enable_explorer = str(received_json["parameter"][0]["enable_explorer"])
try:
# convert ssid list to python list
ssid_list = str(received_json["parameter"][0]["ssid_list"])
ssid_list = ssid_list.replace(" ", "")
ssid_list = ssid_list.split(",")
# convert str to int
ssid_list = list(map(int, ssid_list))
except KeyError:
ssid_list = [0]
# print some debugging parameters
for item in received_json["parameter"][0]:
@ -643,6 +819,8 @@ def process_daemon_commands(data):
tx_audio_level,
respond_to_cq,
rx_buffer_size,
enable_explorer,
ssid_list,
]
)
command_response("start_tnc", True)
@ -738,3 +916,4 @@ def command_response(command, status):
jsondata = {"command_response": command, "status": s_status}
data_out = json.dumps(jsondata)
SOCKET_QUEUE.put(data_out)

View file

@ -11,7 +11,10 @@ Not nice, suggestions are appreciated :-)
import subprocess
from enum import Enum
VERSION = "0.5.0-alpha"
VERSION = "0.6.11-alpha.2"
ENABLE_EXPLORER = False
# DAEMON
DAEMONPORT: int = 3001
@ -22,7 +25,7 @@ TNCPROCESS: subprocess.Popen
MYCALLSIGN: bytes = b"AA0AA"
MYCALLSIGN_CRC: bytes = b"A"
DXCALLSIGN: bytes = b"AA0AA"
DXCALLSIGN: bytes = b"ZZ9YY"
DXCALLSIGN_CRC: bytes = b"A"
MYGRID: bytes = b""
@ -40,7 +43,7 @@ SOCKET_TIMEOUT: int = 1 # seconds
# ---------------------------------
SERIAL_DEVICES: list = []
# ---------------------------------
LISTEN: bool = True
PTT_STATE: bool = False
TRANSMITTING: bool = False
@ -57,6 +60,7 @@ HAMLIB_RADIOCONTROL: str = "direct"
HAMLIB_RIGCTLD_IP: str = "127.0.0.1"
HAMLIB_RIGCTLD_PORT: str = "4532"
HAMLIB_STATUS: str = "unknown/disconnected"
HAMLIB_FREQUENCY: int = 0
HAMLIB_MODE: str = ""
HAMLIB_BANDWIDTH: int = 0
@ -69,6 +73,7 @@ SCATTER: list = []
ENABLE_SCATTER: bool = False
ENABLE_FSK: bool = False
RESPOND_TO_CQ: bool = False
RESPOND_TO_CALL: bool = True # respond to cq, ping, connection request, file request if not in session
# ---------------------------------
# Audio Defaults
@ -77,25 +82,31 @@ AUDIO_INPUT_DEVICES: list = []
AUDIO_OUTPUT_DEVICES: list = []
AUDIO_INPUT_DEVICE: int = -2
AUDIO_OUTPUT_DEVICE: int = -2
AUDIO_RECORD: bool = False
AUDIO_RECORD_FILE = ''
BUFFER_OVERFLOW_COUNTER: list = [0, 0, 0, 0, 0]
AUDIO_RMS: int = 0
AUDIO_DBFS: int = 0
FFT: list = [0]
ENABLE_FFT: bool = False
ENABLE_FFT: bool = True
CHANNEL_BUSY: bool = False
# ARQ PROTOCOL VERSION
ARQ_PROTOCOL_VERSION: int = 2
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
ARQ_SAVE_TO_FOLDER: bool = False
# CHANNEL_STATE = 'RECEIVING_SIGNALLING'
TNC_STATE: str = "IDLE"
@ -149,4 +160,5 @@ class FRAME_TYPE(Enum):
ARQ_DC_OPEN_ACK_N = 228
ARQ_STOP = 249
BEACON = 250
IDENT = 254
TEST_FRAME = 255

104
tools/freedata_cli_tools.py Executable file
View file

@ -0,0 +1,104 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@author: DJ2LS
"""
import argparse
import socket
import base64
import json
from pick import pick
import time
import sounddevice as sd
# --------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description='Simons TEST TNC')
parser.add_argument('--port', dest="socket_port", default=3000, help="Set socket listening port.", type=int)
parser.add_argument('--host', dest="socket_host", default='localhost', help="Set the host, the socket is listening on.", type=str)
args = parser.parse_args()
HOST, PORT = args.socket_host, args.socket_port
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Connect to server
sock.connect((HOST, PORT))
def main_menu():
while True:
time.sleep(0.1)
title = 'Please select a command you want to run: '
options = ['BEACON', 'PING', 'ARQ', 'LIST AUDIO DEVICES']
option, index = pick(options, title)
# BEACON AREA
if option == 'BEACON':
option, index = pick(['5',
'10',
'15',
'30',
'45',
'60',
'90',
'120',
'300',
'600',
'900',
'1800',
'3600',
'STOP BEACON',
'----- BACK -----'], "Select beacon interval [seconds]")
if option == '----- BACK -----':
main_menu()
elif option == 'STOP BEACON':
run_network_command({"type": "broadcast", "command": "stop_beacon"})
else:
run_network_command({"type": "broadcast", "command": "start_beacon", "parameter": str(option)})
elif option == 'PING':
pass
elif option == 'ARQ':
option, index = pick(['GET RX BUFFER', 'DISCONNECT', '----- BACK -----'], "Select ARQ command")
if option == '----- BACK -----':
main_menu()
elif option == 'GET RX BUFFER':
run_network_command({"type": "get", "command": "rx_buffer"})
else:
run_network_command({"type": "arq", "command": "disconnect"})
elif option == 'LIST AUDIO DEVICES':
devices = sd.query_devices(device=None, kind=None)
device_list = []
for device in devices:
device_list.append(
f"{device['index']} - "
f"{sd.query_hostapis(device['hostapi'])['name']} - "
f"Channels (In/Out):{device['max_input_channels']}/{device['max_output_channels']} - "
f"{device['name']}")
device_list.append('----- BACK -----')
option, index = pick(device_list, "Audio devices")
if option == '----- BACK -----':
main_menu()
else:
print("no menu point found...")
def run_network_command(command):
command = json.dumps(command)
command = bytes(command + "\n", 'utf-8')
sock.sendall(command)
if __name__ == "__main__":
main_menu()

View file

@ -0,0 +1,114 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
daemon.py
Author: DJ2LS, January 2022
daemon for providing basic information for the tnc like audio or serial devices
"""
# pylint: disable=invalid-name, line-too-long, c-extension-no-member
# pylint: disable=import-outside-toplevel
import argparse
import socket
import structlog
import queue
import json
import base64
import os
log = structlog.get_logger("CLIENT")
split_char = b"\x00;"
# --------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description='Simons TEST TNC')
parser.add_argument('--port', dest="socket_port", default=3000, help="Set the port, the socket is listening on.", type=int)
parser.add_argument('--host', dest="socket_host", default='localhost', help="Set the host, the socket is listening on.", type=str)
args = parser.parse_args()
ip, port = args.socket_host, args.socket_port
connected = True
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):
decoded_data = base64.b64decode(encoded_data)
decoded_data = decoded_data.split(split_char)
if decoded_data[0] == b'm':
print(jsondata)
log.info(f"{jsondata.get('mycallsign')} <<< {jsondata.get('dxcallsign')}", uuid=decoded_data[3])
log.info(f"{jsondata.get('mycallsign')} <<< {jsondata.get('dxcallsign')}", message=decoded_data[4])
log.info(f"{jsondata.get('mycallsign')} <<< {jsondata.get('dxcallsign')}", filename=decoded_data[5])
log.info(f"{jsondata.get('mycallsign')} <<< {jsondata.get('dxcallsign')}", filetype=decoded_data[6])
log.info(f"{jsondata.get('mycallsign')} <<< {jsondata.get('dxcallsign')}", data=decoded_data[7])
try:
folderpath = "received"
if not os.path.exists(folderpath):
os.makedirs(folderpath)
filename = decoded_data[8].decode("utf-8") + "_" + decoded_data[5].decode("utf-8")
with open(f"{folderpath}/{filename}", "wb") as file:
file.write(decoded_data[7])
with open(f"{folderpath}/{decoded_data[8].decode('utf-8')}_msg.txt", "wb") as file:
file.write(decoded_data[4])
except Exception as e:
print(e)
else:
print(decoded_data)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((ip, port))
print(sock)
while connected:
chunk = sock.recv(1024)
data += chunk
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"")
# iterate through data list
for command in data:
jsondata = json.loads(command)
if jsondata.get('command') == "tnc_state":
pass
if jsondata.get('freedata') == "tnc-message":
log.info(jsondata)
if jsondata.get('ping') == "acknowledge":
log.info(f"PING {jsondata.get('mycallsign')} >><< {jsondata.get('dxcallsign')}", snr=jsondata.get('snr'), dxsnr=jsondata.get('dxsnr'))
if jsondata.get('status') == 'receiving':
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
data = bytes()

117
tools/send_file.py Executable file
View file

@ -0,0 +1,117 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@author: DJ2LS
python3 send_file.py --file cleanup.sh --dxcallsign DN2LS-0 --mycallsign DN2LS-2 --attempts 3
"""
import argparse
import socket
import base64
import json
import uuid
import time
import crcengine
# --------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description='Simons TEST TNC')
parser.add_argument('--port', dest="socket_port", default=3000, help="Set socket listening port.", type=int)
parser.add_argument('--host', dest="socket_host", default='localhost', help="Set the host, the socket is listening on.", type=str)
parser.add_argument('--file', dest="filename", default='', help="Select the file we want to send", type=str)
parser.add_argument('--msg', dest="chatmessage", default='file from cli tool', help="Additional text message appended to file", type=str)
parser.add_argument('--dxcallsign', dest="dxcallsign", default='AA0AA', help="Select the destination callsign", type=str)
parser.add_argument('--mycallsign', dest="mycallsign", default='AA0AA', help="Select the own callsign", type=str)
parser.add_argument('--attempts', dest="attempts", default='5', help="Amount of connection attempts", type=int)
args = parser.parse_args()
HOST, PORT = args.socket_host, args.socket_port
filename = args.filename
dxcallsign = args.dxcallsign
mycallsign = args.mycallsign
attempts = args.attempts
chatmessage = bytes(args.chatmessage, "utf-8")
if filename != "":
# open file by name
f = open(filename, "rb")
file = f.read()
filename = bytes(filename, "utf-8")
else:
file = b""
filename = b""
# convert binary data to base64
#base64_data = base64.b64encode(file).decode("UTF-8")
split_char = b'\0;\1;'
filetype = b"unknown"
timestamp = str(int(time.time()))
# timestamp = timestamp.to_bytes(4, byteorder="big")
timestamp = bytes(timestamp, "utf-8")
msg_with_attachment = timestamp + \
split_char + \
chatmessage + \
split_char + \
filename + \
split_char + \
filetype + \
split_char + \
file
# 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"
command = b"send_message"
checksum = bytes(crc_data.hex(), "utf-8")
uuid_4 = bytes(str(uuid.uuid4()), "utf-8")
data = datatype + \
split_char + \
command + \
split_char + \
checksum + \
split_char + \
uuid_4 + \
split_char + \
msg_with_attachment
data = base64.b64encode(data).decode("UTF-8")
# message
# our command we are going to send
command = {"type": "arq",
"command": "send_raw",
"parameter":
[{"dxcallsign": dxcallsign,
"mycallsign": mycallsign,
"attempts": str(attempts),
"mode": "255",
"n_frames": "1",
"data": data}
]
}
command = json.dumps(command)
print(command)
command = bytes(command + "\n", 'utf-8')
# Create a socket (SOCK_STREAM means a TCP socket)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
# Connect to server and send data
sock.connect((HOST, PORT))
sock.sendall(command)
timeout = time.time() + 5
while time.time() < timeout:
pass

47
tools/send_ping_cq.py Normal file
View file

@ -0,0 +1,47 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@author: DJ2LS
python3 send_file.py --file cleanup.sh --dxcallsign DN2LS-0 --mycallsign DN2LS-2 --attempts 3
"""
import argparse
import socket
import json
# --------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description='Simons TEST TNC')
parser.add_argument('--port', dest="socket_port", default=3000, help="Set socket listening port.", type=int)
parser.add_argument('--host', dest="socket_host", default='localhost', help="Set the host, the socket is listening on.", type=str)
parser.add_argument('--dxcallsign', dest="dxcallsign", default='AA0AA', help="Select the destination callsign", type=str)
parser.add_argument('--mycallsign', dest="mycallsign", default='AA0AA', help="Select the own callsign", type=str)
parser.add_argument('--ping', dest="ping", action="store_true", help="Send PING")
parser.add_argument('--cq', dest="cq", action="store_true", help="Send CQ")
args = parser.parse_args()
HOST, PORT = args.socket_host, args.socket_port
dxcallsign = args.dxcallsign
mycallsign = args.mycallsign
# our command we are going to send
if args.ping:
command = {"type": "ping",
"command": "ping",
"dxcallsign": dxcallsign,
"mycallsign": mycallsign,
}
if args.cq:
command = {"type": "broadcast",
"command": "cqcqcq",
"mycallsign": mycallsign,
}
command = json.dumps(command)
command = bytes(command + "\n", 'utf-8')
# Create a socket (SOCK_STREAM means a TCP socket)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
# Connect to server and send data
sock.connect((HOST, PORT))
sock.sendall(command)