Merge pull request #128 from DJ2LS/ls-leave-prototype

version 0.1
This commit is contained in:
DJ2LS 2022-03-12 16:30:17 +01:00 committed by GitHub
commit be7aad423b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 5603 additions and 8539 deletions

View file

@ -0,0 +1,128 @@
name: Linux
on:
push:
tags:
- 'v*'
jobs:
build_linux_release:
name: Build Linux release
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-20.04]
include:
- os: ubuntu-20.04
zip_name: ubuntu_tnc
generator: Unix Makefiles
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
ref: ls-leave-prototype
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install Linux dependencies
if: matrix.os == 'ubuntu-20.04'
run: |
sudo apt install portaudio19-dev libhamlib-dev libhamlib-utils build-essential cmake python3-libhamlib2
python -m pip install --upgrade pip
pip3 install pyaudio
pip3 install psutil
pip3 install crcengine
pip3 install pyinstaller
pip3 install ujson
pip3 install pyserial
pip3 install numpy
pip3 install structlog
#- name: Build Hamlib Python Binding
# if: matrix.os == 'ubuntu-latest'
# working-directory: tnc
# run: |
# sudo apt install wig libtool
# cd ~
# git clone https://github.com/Hamlib/Hamlib.git
# cd Hamlib
# ./bootstrap
# ./configure --with-python-binding PYTHON_VERSION='3.9' --prefix=$HOME/local
# make
# make install
- name: Build codec2 Linux
if: matrix.os == 'ubuntu-20.04'
working-directory: tnc
run: |
git clone https://github.com/drowe67/codec2.git
cd codec2 && mkdir build_linux && cd build_linux
cmake ../
make
- name: Build Linux Daemon
if: matrix.os == 'ubuntu-20.04'
working-directory: tnc
run: |
pyinstaller freedata.spec
- name: LIST ALL FILES
shell: bash
run: |
ls -R
- name: Compress Linux
shell: bash
run: |
cd ./tnc/dist
mkdir compressed
zip -r ./compressed/${{ matrix.zip_name }}.zip *
- name: Upload Ubuntu TNC artifacts
uses: actions/upload-artifact@v2
with:
name: tnc-artifact
path: ./tnc/dist/compressed/*
- name: Copy TNC to GUI Linux
if: matrix.os == 'ubuntu-20.04'
run: |
cp -R ./tnc/dist/tnc ./gui/tnc
ls -R
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 16
- name: Release TNC
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/v')
with:
files: ./tnc/dist/compressed/${{ matrix.zip_name }}.zip
- 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: "-p always"

117
.github/workflows/build-project-mac.yml vendored Normal file
View file

@ -0,0 +1,117 @@
name: macOS
on:
push:
tags:
- 'v*'
jobs:
build_linux_release:
name: Build macOS release
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-10.15]
include:
- os: macos-10.15
zip_name: mac_tnc
generator: Unix Makefiles
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
ref: ls-leave-prototype
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install macOS dependencies
if: matrix.os == 'macos-10.15'
run: |
python -m pip install --upgrade pip
pip3 install psutil
pip3 install crcengine
pip3 install pyinstaller
pip3 install ujson
pip3 install pyserial
pip3 install numpy
pip3 install structlog
- name: Install Portaudio
if: matrix.os == 'macos-10.15'
run: |
brew install portaudio
pip3 install pyaudio
- name: Build codec2 macOS
if: matrix.os == 'macos-10.15'
working-directory: tnc
run: |
git clone https://github.com/drowe67/codec2.git
cd codec2 && mkdir build_mac && cd build_mac
cmake ../
make
- name: Build macOS pyinstaller
if: matrix.os == 'macos-10.15'
working-directory: tnc
run: |
pyinstaller freedata.spec
- name: LIST ALL FILES
shell: bash
run: |
ls -R
- name: Compress
shell: bash
run: |
cd ./tnc/dist
mkdir compressed
zip -r ./compressed/${{ matrix.zip_name }}.zip *
- name: Upload macOS TNC artifacts
uses: actions/upload-artifact@v2
with:
name: tnc-artifact
path: ./tnc/dist/compressed/*
- name: Copy TNC to GUI
if: matrix.os == 'macos-10.15'
run: |
cp -R ./tnc/dist/tnc ./gui/tnc
ls -R
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 16
- name: Release TNC
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/v')
with:
files: ./tnc/dist/compressed/${{ matrix.zip_name }}.zip
- 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: "-p always"

115
.github/workflows/build-project-win.yml vendored Normal file
View file

@ -0,0 +1,115 @@
name: Windows
on:
push:
tags:
- 'v*'
jobs:
build_windows_release:
name: Build Windows release
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest]
include:
- os: windows-latest
zip_name: windows_tnc
generator: Visual Studio 16 2019
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
ref: ls-leave-prototype
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install Python dependencies
if: matrix.os == 'windows-latest'
run: |
python -m pip install --upgrade pip
pip install psutil
pip install crcengine
pip install pyinstaller
pip install ujson
pip install pyserial
pip install numpy
pip install structlog
pip install colorama
# curl.exe --output PyAudio-0.2.11-cp39-cp39-win_amd64.whl --url https://download.lfd.uci.edu/pythonlibs/y2rycu7g/PyAudio-0.2.11-cp39-cp39-win_amd64.whl
# pip install PyAudio-0.2.11-cp39-cp39-win_amd64.whl
- name: Install Pyaudio
if: matrix.os == 'windows-latest'
working-directory: tnc/lib/pyaudio/windows
run: |
pip install PyAudio-0.2.11-cp39-cp39-win_amd64.whl
- name: Build Windows Daemon and TNC
if: matrix.os == 'windows-latest'
working-directory: tnc
run: |
pyinstaller freedata.spec
- name: Copy TNC to GUI
shell: bash
run: |
cp -R ./tnc/dist/tnc ./gui/tnc
- name: LIST ALL FILES
shell: bash
run: |
ls -R
- name: Archive TNC
uses: thedoctor0/zip-release@master
with:
type: 'zip'
filename: '${{ matrix.zip_name }}.zip'
directory: ./tnc/dist/tnc
path: .
#exclusions: '*.git* /*node_modules/* .editorconfig'
- name: LIST ALL FILES
shell: bash
working-directory: tnc
run: |
ls -R
#- name: Build codec2
# shell: bash
# run: |
# choco install ninja cmake
# ninja --version
# cmake --version
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 16
- name: Release TNC
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/v')
with:
files: ./tnc/dist/tnc/${{ matrix.zip_name }}.zip
- 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: "-p always"

View file

@ -1,308 +0,0 @@
name: Build/PROJECT
on:
push:
#tags:
#- '*'
jobs:
build_windows_release:
name: Build Windows release
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest]
include:
- os: windows-latest
zip_name: windows_tnc
generator: Visual Studio 16 2019
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install Python dependencies
if: matrix.os == 'windows-latest'
run: |
python -m pip install --upgrade pip
pip install psutil
pip install crcengine
pip install pyinstaller
pip install ujson
pip install pyserial
pip install numpy
pip install structlog
pip install colorama
# curl.exe --output PyAudio-0.2.11-cp39-cp39-win_amd64.whl --url https://download.lfd.uci.edu/pythonlibs/y2rycu7g/PyAudio-0.2.11-cp39-cp39-win_amd64.whl
# pip install PyAudio-0.2.11-cp39-cp39-win_amd64.whl
- name: Install Pyaudio
if: matrix.os == 'windows-latest'
working-directory: tnc/lib/pyaudio/windows
run: |
pip install PyAudio-0.2.11-cp39-cp39-win_amd64.whl
- name: Build Windows Daemon
if: matrix.os == 'windows-latest'
working-directory: tnc
run: |
pyinstaller -F --add-binary="lib/codec2/windows/*;lib/codec2/windows/" --add-binary="lib/hamlib/win32/*;lib/hamlib/win32" --add-binary="lib/hamlib/win64/*;lib/hamlib/win64" daemon.py -n daemon
- name: Build Windows TNC
if: matrix.os == 'windows-latest'
working-directory: tnc
run: |
pyinstaller -F --add-binary="lib/codec2/windows/*;lib/codec2/windows/" --add-binary="lib/hamlib/win32/*;lib/hamlib/win32" --add-binary="lib/hamlib/win64/*;lib/hamlib/win64" main.py -n tnc
- name: LIST ALL FILES
shell: bash
run: |
ls -R
#- name: Build codec2
# shell: bash
# run: |
# choco install ninja cmake
# ninja --version
# cmake --version
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 14
- name: Copy TNC and DAEMOn to GUI Windows
if: matrix.os == 'windows-latest'
run: |
cp -R ./tnc/dist ./gui/
ls -R
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:
package_root: "./gui/"
github_token: ${{ secrets.github_token }}
- name: LIST ALL FILES
shell: bash
run: |
ls -R
- name: chmod +x
shell: bash
run: |
chmod +x ./gui/dist/*.*
- name: Create app bundle
shell: bash
run: |
mkdir gui-bundle
cp ./gui/dist/*.* gui-bundle
cp ./tnc/dist/daemon.exe gui-bundle
cp ./tnc/dist/tnc.exe gui-bundle
chmod +x ./gui-bundle/tnc
chmod +x ./gui-bundle/daemon
cd ./gui-bundle
ls -R
- name: Archive Release
uses: thedoctor0/zip-release@master
with:
type: 'zip'
filename: 'FreeDATA_windows.zip'
directory: ./gui-bundle/
path: .
#exclusions: '*.git* /*node_modules/* .editorconfig'
- name: Upload Windows GUI Bundle artifacts
uses: actions/upload-artifact@v2
with:
name: gui-bundle-windows-artifact
path: ./gui-bundle/FreeDATA_windows.zip
build_linux_release:
name: Build Linux release
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-20.04]
include:
- os: ubuntu-20.04
zip_name: ubuntu_tnc
generator: Unix Makefiles
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install Linux dependencies
if: matrix.os == 'ubuntu-20.04'
run: |
sudo apt install portaudio19-dev libhamlib-dev libhamlib-utils build-essential cmake python3-libhamlib2
python -m pip install --upgrade pip
pip3 install pyaudio
pip3 install psutil
pip3 install crcengine
pip3 install pyinstaller
pip3 install ujson
pip3 install pyserial
pip3 install numpy
pip3 install structlog
#- name: Build Hamlib Python Binding
# if: matrix.os == 'ubuntu-latest'
# working-directory: tnc
# run: |
# sudo apt install wig libtool
# cd ~
# git clone https://github.com/Hamlib/Hamlib.git
# cd Hamlib
# ./bootstrap
# ./configure --with-python-binding PYTHON_VERSION='3.9' --prefix=$HOME/local
# make
# make install
- name: Build codec2 Linux
if: matrix.os == 'ubuntu-20.04'
working-directory: tnc
run: |
git clone https://github.com/drowe67/codec2.git
cd codec2 && mkdir build_linux && cd build_linux
cmake ../
make
- name: Build Linux Daemon
if: matrix.os == 'ubuntu-20.04'
working-directory: tnc
run: |
pyinstaller freedata.spec
- name: LIST ALL FILES
shell: bash
run: |
ls -R
- name: Compress Linux
shell: bash
run: |
cd ./tnc/dist
mkdir compressed
zip -r ./compressed/${{ matrix.zip_name }}.zip *
- name: Upload Ubuntu TNC artifacts
uses: actions/upload-artifact@v2
with:
name: tnc-artifact
path: ./tnc/dist/compressed/*
- name: Copy TNC to GUI Linux
if: matrix.os == 'ubuntu-20.04'
run: |
cp -R ./tnc/dist/tnc ./gui/tnc
ls -R
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v1
with:
node-version: 14
- name: Build/release Electron app
uses: samuelmeuli/action-electron-builder@v1
with:
package_root: "./gui/"
github_token: ${{ secrets.github_token }}
- name: LIST ALL FILES
shell: bash
run: |
ls -R
- name: chmod +x
shell: bash
run: |
chmod +x ./gui/dist/*.AppImage
- name: Create app bundle
shell: bash
run: |
mkdir gui-bundle
cp ./gui/dist/*.AppImage gui-bundle
cp -R ./tnc/dist/tnc gui-bundle
chmod +x ./gui-bundle/tnc/tnc
chmod +x ./gui-bundle/tnc/daemon
cd ./gui-bundle
zip -r FreeDATA_linux.zip .
ls -R
- name: Upload Ubuntu GUI Bundle artifacts
uses: actions/upload-artifact@v2
with:
name: gui-bundle-ubuntu-artifact
path: ./gui-bundle/FreeDATA_linux.zip
release:
name: Upload Release
needs: [build_linux_release, build_windows_release]
runs-on: ubuntu-20.04
steps:
- name: Download artifact
uses: actions/download-artifact@v2
- name: LIST ALL FILES
shell: bash
run: |
ls -R
- name: Release
uses: WebFreak001/deploy-nightly@v1.1.0
#uses: softprops/action-gh-release@v1
#if: startsWith(github.ref, 'refs/tags/')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: https://uploads.github.com/repos/DJ2LS/FreeDATA/releases/48616289/assets{?name,label} # find out this value by opening https://api.github.com/repos/<owner>/<repo>/releases in your browser and copy the full "upload_url" value including the {?name,label} part
release_id: 48616289 # same as above (id can just be taken out the upload_url, it's used to find old releases)
asset_path: ./gui-bundle-ubuntu-artifact/FreeDATA_linux.zip # path to archive to upload
asset_name: FreeDATA_linux-nightly-$$.zip # name to upload the release as, use $$ to insert date (YYYYMMDD) and 6 letter commit hash
asset_content_type: application/zip # required by GitHub API
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
#files: |
# ./gui-bundle-ubuntu-artifact/codec2-FreeDATA_ubuntu.zip
# ./tnc-artifact/ubuntu_tnc.zip
# LICENSE

4
.stignore Normal file
View file

@ -0,0 +1,4 @@
tnc/__pycache__
tnc/daemon.log
tnc/tnc.log
gui/node_modules

View file

@ -4,9 +4,16 @@ My attempt to create a free and open-source TNC with a GUI for [codec2](https://
[mailing-list](https://groups.io/g/freedata)
## Under development
![Build Windows](https://github.com/DJ2LS/FreeDATA/actions/workflows/build-project-win.yml/badge.svg)
![Build Linux](https://github.com/DJ2LS/FreeDATA/actions/workflows/build-project-linux.yml/badge.svg)
![Build macOS](https://github.com/DJ2LS/FreeDATA/actions/workflows/build-project-mac.yml/badge.svg)
Please keep in mind, that this project is still a prototype with many issues which need to be solved.
Build steps for other OS than Ubuntu are provided, but not fully working, yet.
Please check the [Releases](https://github.com/DJ2LS/FreeDATA/releases) section for downloading nightly builds
## Preview
![preview](https://github.com/DJ2LS/FreeDATA/blob/main/documentation/FreeDATA_preview.gif?raw=true "Preview")
@ -20,7 +27,7 @@ xssfox : https://github.com/xssfox/freedv-tnc
## Running the Ubuntu app bundle
Download the latest developer release from the releases section, unpack it and just start the ".AppImage file". No more dependencies
## Manual installation
Please check the [wiki](https://github.com/DJ2LS/FreeDATA/wiki) for installation instructions
## Installation
Please check the [wiki](https://wiki.freedata.app) for installation instructions

BIN
documentation/cube.xcf Normal file

Binary file not shown.

View file

@ -1,8 +0,0 @@
{
"tnc_host" : "127.0.0.1",
"tnc_port" : "3000",
"daemon_host" : "127.0.0.1",
"daemon_port" : "3001",
"mycall" : "AA0AA",
"mygrid" : "AA11ea"
}

View file

@ -3,6 +3,10 @@ const path = require('path')
const {
ipcRenderer
} = require('electron')
const log = require('electron-log');
const daemonLog = log.scope('daemon');
// https://stackoverflow.com/a/26227660
var appDataFolder = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME + "/.config")
@ -11,16 +15,17 @@ var configPath = path.join(configFolder, 'config.json')
const config = require(configPath);
var daemon = new net.Socket();
var msg = ''; // Current message, per connection.
var socketchunk = ''; // Current message, per connection.
setTimeout(connectDAEMON, 500)
function connectDAEMON() {
console.log('connecting to DAEMON...')
daemonLog.info('connecting to daemon');
//clear message buffer after reconnecting or inital connection
msg = '';
socketchunk = '';
if (config.tnclocation == 'localhost') {
daemon.connect(3001, '127.0.0.1')
@ -32,25 +37,48 @@ function connectDAEMON() {
//client.setTimeout(5000);
}
daemon.on('connect', function(data) {
console.log('DAEMON connection established')
daemon.on('connect', function(err) {
daemonLog.info('daemon connection established');
let Data = {
daemon_connection: daemon.readyState,
};
ipcRenderer.send('request-update-daemon-connection', Data);
})
daemon.on('error', function(data) {
console.log('DAEMON connection error');
setTimeout(connectDAEMON, 2000)
daemon.on('error', function(err) {
daemonLog.error('daemon connection error');
daemonLog.error(err)
daemon.destroy();
setTimeout(connectDAEMON, 1000)
let Data = {
daemon_connection: daemon.readyState,
};
ipcRenderer.send('request-update-daemon-connection', Data);
});
/*
client.on('close', function(data) {
console.log(' TNC connection closed');
setTimeout(connectTNC, 2000)
let Data = {
daemon_connection: daemon.readyState,
};
ipcRenderer.send('request-update-daemon-connection', Data);
});
*/
daemon.on('end', function(data) {
console.log('DAEMON connection ended');
setTimeout(connectDAEMON, 2000)
daemonLog.warn('daemon connection ended');
daemon.destroy();
setTimeout(connectDAEMON, 500)
let Data = {
daemon_connection: daemon.readyState,
};
ipcRenderer.send('request-update-daemon-connection', Data);
});
//exports.writeCommand = function(command){
@ -79,50 +107,91 @@ writeDaemonCommand = function(command) {
// "https://stackoverflow.com/questions/9070700/nodejs-net-createserver-large-amount-of-data-coming-in"
daemon.on('data', function(data) {
daemon.on('data', function(socketdata) {
data = data.toString('utf8'); /* convert data to string */
msg += data.toString('utf8'); /*append data to buffer so we can stick long data together */
/*
inspired by:
stackoverflow.com questions 9070700 nodejs-net-createserver-large-amount-of-data-coming-in
*/
/* check if we reached an EOF, if true, clear buffer and parse JSON data */
if (data.endsWith('}')) {
/*console.log(msg)*/
try {
/*console.log(msg)*/
data = JSON.parse(msg)
} catch (e) {
console.log(e); /* "SyntaxError */
}
msg = '';
/*console.log("EOF detected!")*/
if (data['COMMAND'] == 'DAEMON_STATE') {
let Data = {
input_devices: data['INPUT_DEVICES'],
output_devices: data['OUTPUT_DEVICES'],
python_version: data['PYTHON_VERSION'],
hamlib_version: data['HAMLIB_VERSION'],
serial_devices: data['SERIAL_DEVICES'],
tnc_running_state: data['DAEMON_STATE'][0]['STATUS'],
ram_usage: data['RAM'],
cpu_usage: data['CPU'],
version: data['VERSION'],
};
ipcRenderer.send('request-update-daemon-state', Data);
}
socketdata = socketdata.toString('utf8'); // convert data to string
socketchunk += socketdata// append data to buffer so we can stick long data together
if (data['COMMAND'] == 'TEST_HAMLIB') {
let Data = {
hamlib_result: data['RESULT'],
};
ipcRenderer.send('request-update-hamlib-test', Data);
// check if we received begin and end of json data
if (socketchunk.startsWith('{"') && socketchunk.endsWith('"}\n')) {
var data = ''
// split data into chunks if we received multiple commands
socketchunk = socketchunk.split("\n");
data = JSON.parse(socketchunk[0])
// search for empty entries in socketchunk and remove them
for (i = 0; i < socketchunk.length; i++) {
if (socketchunk[i] === ''){
socketchunk.splice(i, 1);
}
}
////// check if EOF ...
//iterate through socketchunks array to execute multiple commands in row
for (i = 0; i < socketchunk.length; i++) {
//check if data is not empty
if(socketchunk[i].length > 0){
//try to parse JSON
try {
data = JSON.parse(socketchunk[i])
} catch (e) {
console.log(e); // "SyntaxError
daemonLog.error(e);
daemonLog.debug(socketchunk[i])
socketchunk = ''
}
}
if (data['command'] == 'daemon_state') {
let Data = {
input_devices: data['input_devices'],
output_devices: data['output_devices'],
python_version: data['python_version'],
hamlib_version: data['hamlib_version'],
serial_devices: data['serial_devices'],
tnc_running_state: data['daemon_state'][0]['status'],
ram_usage: data['ram'],
cpu_usage: data['cpu'],
version: data['version'],
};
ipcRenderer.send('request-update-daemon-state', Data);
}
if (data['command'] == 'test_hamlib') {
let Data = {
hamlib_result: data['result'],
};
ipcRenderer.send('request-update-hamlib-test', Data);
}
}
//finally delete message buffer
socketchunk = '';
}
});
function hexToBytes(hex) {
@ -133,17 +202,17 @@ function hexToBytes(hex) {
exports.getDaemonState = function() {
//function getDaemonState(){
command = '{"type" : "GET", "command" : "DAEMON_STATE"}'
command = '{"type" : "get", "command" : "daemon_state"}'
writeDaemonCommand(command)
}
// 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) {
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_bandwith_mode) {
var json_command = JSON.stringify({
type: 'SET',
command: 'STARTTNC',
type: 'set',
command: 'start_tnc',
parameter: [{
mycall: mycall,
mygrid: mygrid,
@ -159,18 +228,21 @@ exports.startTNC = function(mycall, mygrid, rx_audio, tx_audio, radiocontrol, de
stop_bits: stop_bits,
handshake: handshake,
rigctld_port: rigctld_port,
rigctld_ip: rigctld_ip
rigctld_ip: rigctld_ip,
enable_scatter: enable_scatter,
enable_fft: enable_fft,
low_bandwith_mode : low_bandwith_mode
}]
})
//console.log(json_command)
daemonLog.debug(json_command);
writeDaemonCommand(json_command)
}
// STOP TNC
exports.stopTNC = function() {
command = '{"type" : "SET", "command": "STOPTNC" , "parameter": "---" }'
command = '{"type" : "set", "command": "stop_tnc" , "parameter": "---" }'
writeDaemonCommand(command)
}
@ -178,8 +250,8 @@ exports.stopTNC = function() {
exports.testHamlib = function(radiocontrol, devicename, deviceport, serialspeed, pttprotocol, pttport, data_bits, stop_bits, handshake, rigctld_ip, rigctld_port) {
var json_command = JSON.stringify({
type: 'GET',
command: 'TEST_HAMLIB',
type: 'get',
command: 'test_hamlib',
parameter: [{
radiocontrol: radiocontrol,
devicename: devicename,
@ -194,7 +266,7 @@ exports.testHamlib = function(radiocontrol, devicename, deviceport, serialspeed,
rigctld_ip: rigctld_ip
}]
})
console.log(json_command)
daemonLog.debug(json_command);
writeDaemonCommand(json_command)
}
@ -202,13 +274,13 @@ exports.testHamlib = function(radiocontrol, devicename, deviceport, serialspeed,
//Save myCall
exports.saveMyCall = function(callsign) {
command = '{"type" : "SET", "command": "MYCALLSIGN" , "parameter": "' + callsign + '", "timestamp" : "' + Date.now() + '"}'
command = '{"type" : "set", "command": "mycallsign" , "parameter": "' + callsign + '"}'
writeDaemonCommand(command)
}
// Save myGrid
exports.saveMyGrid = function(grid) {
command = '{"type" : "SET", "command": "MYGRID" , "parameter": "' + grid + '", "timestamp" : "' + Date.now() + '"}'
command = '{"type" : "set", "command": "mygrid" , "parameter": "' + grid + '"}'
writeDaemonCommand(command)
}

View file

@ -1,59 +1,112 @@
const {
app,
BrowserWindow,
ipcMain
} = require('electron')
const path = require('path')
const fs = require('fs')
ipcMain,
dialog,
shell
} = require('electron');
const { autoUpdater } = require('electron-updater');
const path = require('path');
const fs = require('fs');
const os = require('os');
const exec = require('child_process').spawn;
const log = require('electron-log');
const mainLog = log.scope('main');
const daemonProcessLog = log.scope('freedata-daemon');
const sysInfo = log.scope('system information');
sysInfo.info("SYSTEM INFORMATION ----------------------------- ");
sysInfo.info("APP VERSION : " + app.getVersion());
sysInfo.info("PLATFORM : " + os.platform());
sysInfo.info("ARCHITECTURE: " + os.arch());
sysInfo.info("FREE MEMORY: " + os.freemem());
sysInfo.info("TOTAL MEMORY: " + os.totalmem());
sysInfo.info("LOAD AVG : " + os.loadavg());
sysInfo.info("RELEASE : " + os.release());
sysInfo.info("TYPE : " + os.type());
sysInfo.info("VERSION : " + os.version());
sysInfo.info("UPTIME : " + os.uptime());
app.setName("FreeDATA");
var appDataFolder = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME + "/.config")
var appDataFolder = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME + "/.config");
var configFolder = path.join(appDataFolder, "FreeDATA");
var configPath = path.join(configFolder, 'config.json')
var configPath = path.join(configFolder, 'config.json');
// create config folder if not exists
if (!fs.existsSync(configFolder)) {
fs.mkdirSync(configFolder);
}
// create config file if not exists
var configContent = `
{
"tnc_host": "127.0.0.1",
"tnc_port": "3000",
"daemon_host": "127.0.0.1",
"daemon_port": "3001",
"mycall": "AA0AA",
"mygrid": "JN40aa",
"deviceid": "RIG_MODEL_DUMMY_NOVFO",
"deviceport": "/dev/ttyACM1",
"serialspeed": "9600",
"ptt": "USB",
"spectrum": "waterfall",
"tnclocation": "localhost",
"stop_bits" : "1",
"data_bits" : "8",
"handshake" : "None",
"radiocontrol" : "direct",
"deviceport_rigctl" : "3",
"deviceid_rigctl" : "3",
"serialspeed_rigctl" : "9600",
"pttprotocol_rigctl" : "USB",
"rigctld_port" : "4532",
"rigctld_ip" : "127.0.0.1"
// create config file if not exists with defaults
const configDefaultSettings = '{\
"tnc_host": "127.0.0.1",\
"tnc_port": "3000",\
"daemon_host": "127.0.0.1",\
"daemon_port": "3001",\
"mycall": "AA0AA-0",\
"mygrid": "JN40aa",\
"deviceid": "RIG_MODEL_DUMMY_NOVFO",\
"deviceport": "/dev/ttyACM1",\
"serialspeed_direct": "9600",\
"spectrum": "waterfall",\
"tnclocation": "localhost",\
"stop_bits_direct" : "1",\
"data_bits_direct" : "8",\
"handshake_direct" : "None",\
"radiocontrol" : "disabled",\
"deviceport_rigctl" : "3",\
"deviceid_rigctl" : "3",\
"serialspeed_rigctl" : "9600",\
"pttprotocol_direct" : "USB",\
"pttprotocol_rigctl" : "USB",\
"rigctld_port" : "4532",\
"rigctld_ip" : "127.0.0.1",\
"enable_scatter" : "False",\
"enable_fft" : "False",\
"low_bandwith_mode" : "False",\
"theme" : "default",\
"screen_height" : 430,\
"screen_width" : 1050,\
"update_channel" : "latest",\
"beacon_interval" : 5,\
"received_files_folder" : "None"\
}';
}
`;
if (!fs.existsSync(configPath)) {
fs.writeFileSync(configPath, configContent)
fs.writeFileSync(configPath, configDefaultSettings)
}
// load settings
var config = require(configPath);
//config validation
// check running config against default config.
// if parameter not exists, add it to running config to prevent errors
sysInfo.info("CONFIG VALIDATION ----------------------------- ");
var parsedConfig = JSON.parse(configDefaultSettings);
for (key in parsedConfig) {
if (config.hasOwnProperty(key)) {
sysInfo.info("FOUND SETTTING [" + key + "]: " + config[key]);
} else {
sysInfo.error("MISSING SETTTING [" + key + "] : " + parsedConfig[key]);
config[key] = parsedConfig[key];
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
}
}
sysInfo.info("------------------------------------------ ");
/*
var chatDB = path.join(configFolder, 'chatDB.json')
// create chat database file if not exists
var configContent = `
const configContentChatDB = `
{ "chatDB" : [{
"id" : "00000000",
"timestamp" : 1234566,
@ -65,9 +118,9 @@ var configContent = `
}
`;
if (!fs.existsSync(chatDB)) {
fs.writeFileSync(chatDB, configContent)
fs.writeFileSync(chatDB, configContentChatDB);
}
*/
/*
@ -88,25 +141,29 @@ fs.mkdir(receivedFilesFolder, {
const config = require(configPath);
const exec = require('child_process').exec;
let win = null;
let data = null;
let logViewer = null;
var daemonProcess = null;
function createWindow() {
win = new BrowserWindow({
width: 1050,
height: 430,
width: config.screen_width,
height: config.screen_height,
autoHideMenuBar: true,
icon: __dirname + '/src/icon_cube_border.png',
icon: 'src/img/icon.png',
webPreferences: {
//preload: path.join(__dirname, 'preload-main.js'),
preload: require.resolve('./preload-main.js'),
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: false, //https://stackoverflow.com/questions/53390798/opening-new-window-electron/53393655 https://github.com/electron/remote
enableRemoteModule: false,
sandbox: false
//https://stackoverflow.com/questions/53390798/opening-new-window-electron/53393655
//https://github.com/electron/remote
}
})
// hide menu bar
@ -132,90 +189,219 @@ function createWindow() {
}
})
chat.loadFile('src/chat-module.html')
chat.setMenuBarVisibility(false)
chat.loadFile('src/chat-module.html');
chat.setMenuBarVisibility(false);
logViewer = new BrowserWindow({
height: 900,
width: 600,
show: false,
parent: win,
webPreferences: {
preload: require.resolve('./preload-log.js'),
nodeIntegration: true,
}
})
logViewer.loadFile('src/log-module.html');
logViewer.setMenuBarVisibility(false);
// Emitted when the window is closed.
logViewer.on('close', function(evt) {
evt.preventDefault();
logViewer.hide();
})
// Emitted when the window is closed.
win.on('closed', function() {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
win = null;
chat = null;
logViewer = null;
})
win.once('ready-to-show', () => {
log.transports.file.level = "debug"
autoUpdater.logger = log.scope('updater');
autoUpdater.channel = config.update_channel
autoUpdater.autoInstallOnAppQuit = false;
autoUpdater.autoDownload = true;
autoUpdater.checkForUpdatesAndNotify();
//autoUpdater.quitAndInstall();
});
chat.on('closed', function () {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
})
// https://stackoverflow.com/questions/44258831/only-hide-the-window-when-closing-it-electron
chat.on('close', function(evt) {
evt.preventDefault();
chat.hide()
chat.hide();
});
}
app.whenReady().then(() => {
createWindow()
createWindow();
// start daemon by checking os
// https://stackoverflow.com/a/5775120
console.log("Trying to start daemon binary")
mainLog.info('Starting freedata-daemon binary');
if(os.platform()=='darwin'){
daemonProcess = exec(path.join(process.resourcesPath, 'tnc', 'freedata-daemon'), [],
{
cwd: path.join(process.resourcesPath, 'tnc'),
});
}
if(os.platform()=='linux' || os.platform()=='darwin'){
daemonProcess = exec('./tnc/daemon', function callback(err, stdout, stderr) {
if (err) {
console.log(os.platform())
console.error(err)
console.error("Can't start daemon binary");
console.error("--> this is only working with the app bundle and a precompiled binaries");
return;
}
console.log(stdout);
});
/*
process.resourcesPath -->
/tmp/.mount_FreeDAUQYfKb/resources
__dirname -->
/tmp/.mount_FreeDAUQYfKb/resources/app.asar
*/
if(os.platform()=='linux'){
/*
var folder = path.join(process.resourcesPath, 'tnc');
//var folder = path.join(__dirname, 'extraResources', 'tnc');
console.log(folder);
fs.readdir(folder, (err, files) => {
console.log(files);
});
*/
daemonProcess = exec(path.join(process.resourcesPath, 'tnc', 'freedata-daemon'), [],
{
cwd: path.join(process.resourcesPath, 'tnc'),
});
}
if(os.platform()=='win32' || os.platform()=='win64'){
daemonProcess = exec('./tnc/daemon.exe', function callback(err, stdout, stderr) {
if (err) {
console.log(os.platform())
console.error(err)
console.error("Can't start daemon binary");
console.error("--> this is only working with the app bundle and a precompiled binaries");
return;
}
console.log(stdout);
});
// for windows the relative path via path.join(__dirname) is not needed for some reason
//daemonProcess = exec('\\tnc\\daemon.exe', [])
daemonProcess = exec(path.join(process.resourcesPath, 'tnc', 'freedata-daemon.exe'), [],
{
cwd: path.join(process.resourcesPath, 'tnc'),
});
}
// return process messages
daemonProcess.on('error', (err) => {
daemonProcessLog.error(`error when starting daemon: ${err}`);
});
daemonProcess.on('message', (data) => {
daemonProcessLog.info(`${data}`);
});
daemonProcess.stdout.on('data', (data) => {
daemonProcessLog.info(`${data}`);
});
daemonProcess.stderr.on('data', (data) => {
daemonProcessLog.info(`${data}`);
let arg = {
entry: `${data}`
};
// send info to log only if log screen available
// it seems an error occurs when updating
if (logViewer !== null && logViewer !== ''){
logViewer.webContents.send('action-update-log', arg);
}
});
daemonProcess.on('close', (code) => {
daemonProcessLog.warn(`daemonProcess exited with code ${code}`);
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow()
createWindow();
}
})
})
app.on('window-all-closed', () => {
// kill daemon process
daemonProcess.kill('SIGINT');
if (process.platform !== 'darwin') {
app.quit()
// closing the tnc binary if not closed when closing application and also our daemon which has been started by the gui
try {
daemonProcess.kill();
} catch (e) {
mainLog.error(e)
}
mainLog.warn('closing tnc');
if(os.platform()=='win32' || os.platform()=='win64'){
exec('Taskkill', ['/IM', 'freedata-tnc.exe', '/F'])
}
if(os.platform()=='linux'){
exec('pkill', ['-9', 'freedata-tnc'])
// on macOS we need to kill the daemon as well. If we are not doing this,
// the daemon wont startup again because the socket is already in use
//for some reason killing the daemon is killing our screen on Ubuntu..it seems theres another "daemon" out there...
exec('pkill', ['-9', 'freedata-daemon'])
}
if(os.platform()=='darwin'){
exec('pkill', ['-9', 'freedata-tnc'])
// on macOS we need to kill the daemon as well. If we are not doing this,
// the daemon wont startup again because the socket is already in use
//for some reason killing the daemon is killing our screen on Ubuntu..it seems theres another "daemon" out there...
exec('pkill', ['-9', 'freedata-daemon'])
}
/*
if (process.platform !== 'darwin') {
app.quit();
}
*/
mainLog.warn('quitting app');
app.quit();
})
// IPC HANDLER
ipcMain.on('request-show-chat-window', (event, arg) => {
chat.show()
chat.show();
});
@ -244,6 +430,9 @@ ipcMain.on('request-update-hamlib-test', (event, arg) => {
ipcMain.on('request-update-tnc-connection', (event, arg) => {
win.webContents.send('action-update-tnc-connection', arg);
});
ipcMain.on('request-update-daemon-connection', (event, arg) => {
win.webContents.send('action-update-daemon-connection', arg);
@ -257,6 +446,99 @@ ipcMain.on('request-update-rx-buffer', (event, arg) => {
win.webContents.send('action-update-rx-buffer', arg);
});
/*
ipcMain.on('request-update-rx-msg-buffer', (event, arg) => {
chat.webContents.send('action-update-rx-msg-buffer', arg);
});
*/
ipcMain.on('request-new-msg-received', (event, arg) => {
chat.webContents.send('action-new-msg-received', arg);
});
ipcMain.on('request-open-tnc-log', (event) => {
logViewer.show();
});
//folder selector
ipcMain.on('get-folder-path',(event,data)=>{
dialog.showOpenDialog({defaultPath: path.join(__dirname, '../assets/'),
buttonLabel: 'Select folder', properties: ['openDirectory']}).then(folderPaths => {
win.webContents.send('return-folder-paths', {path: folderPaths,})
});
});
//open folder
ipcMain.on('open-folder',(event,data)=>{
shell.showItemInFolder(data.path)
});
// LISTENER FOR UPDATER EVENTS
autoUpdater.on('update-available', (info) => {
mainLog.info('update available');
let arg = {
status: "update-available",
info: info
};
win.webContents.send('action-updater', arg);
});
autoUpdater.on('update-not-available', (info) => {
mainLog.info('update not available');
let arg = {
status: "update-not-available",
info: info
};
win.webContents.send('action-updater', arg);
});
autoUpdater.on('update-downloaded', (info) => {
mainLog.info('update downloaded');
let arg = {
status: "update-downloaded",
info: info
};
win.webContents.send('action-updater', arg);
// we need to call this at this point.
// if an update is available and we are force closing the app
// the entire screen crashes...
mainLog.info('quit application and install update');
autoUpdater.quitAndInstall();
});
autoUpdater.on('checking-for-update', () => {
mainLog.info('checking for update');
let arg = {
status: "checking-for-update",
version: app.getVersion()
};
win.webContents.send('action-updater', arg);
});
autoUpdater.on('download-progress', (progress) => {
let arg = {
status: "download-progress",
progress: progress
};
win.webContents.send('action-updater', arg);
});
autoUpdater.on('error', (error) => {
mainLog.info('update error');
let arg = {
status: "error",
progress: error
};
win.webContents.send('action-updater', arg);
});

6170
gui/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "FreeDATA",
"version": "0.0.1",
"version": "0.1.2-alpha",
"description": "FreeDATA ",
"main": "main.js",
"scripts": {
@ -8,12 +8,12 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"engines": {
"node": ">=12.0.0",
"node": ">=14.0.0",
"npm": ">=6.0.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/DJ2LS/FreeDATA.git"
"url": "https://github.com/DJ2LS/FreeDATA.git"
},
"keywords": [
"TNC",
@ -26,15 +26,64 @@
"bugs": {
"url": "https://github.com/DJ2LS/FreeDATA/issues"
},
"homepage": "https://github.com/DJ2LS/FreeDATA#readme",
"homepage": "https://freedata.app",
"dependencies": {
"bootstrap": "^5.1.0",
"bootstrap-icons": "^1.8.1",
"bootswatch": "^5.1.3",
"chart.js": "^3.5.1",
"chartjs-plugin-annotation": "^1.0.2",
"qth-locator": "^2.1.0"
"electron-log": "^4.4.6",
"electron-updater": "^5.0.0",
"pouchdb": "^7.2.2",
"qth-locator": "^2.1.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"electron": "^15.0.0",
"electron-builder": "^22.11.7"
"electron": "^17.0.0",
"electron-builder": "^22.14.13"
},
"build": {
"productName": "FreeDATA",
"appId": "app.freedata",
"directories": {
"buildResources": "src/img",
"output": "dist"
},
"dmg": {
"icon": "src/img/icon.png",
"contents": [
{
"x": 130,
"y": 220
},
{
"x": 410,
"y": 220,
"type": "link",
"path": "/Applications"
}
]
},
"win": {
"icon": "src/img/icon.png",
"target": [
"nsis"
]
},
"linux": {
"icon": "src/img/icon.png",
"target": [
"AppImage"
],
"category": "Development"
},
"publish": {
"provider": "github",
"releaseType": "release"
},
"extraResources": [
"./tnc/**"
]
}
}

View file

@ -3,87 +3,293 @@ const {
ipcRenderer
} = require('electron')
const { v4: uuidv4 } = require('uuid');
// https://stackoverflow.com/a/26227660
var appDataFolder = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME + "/.config")
var configFolder = path.join(appDataFolder, "FreeDATA");
var configPath = path.join(configFolder, 'config.json')
const config = require(configPath);
var chatDB = path.join(configFolder, 'chatDB.json')
// set date format
const dateFormat = new Intl.DateTimeFormat('en-GB', {
timeStyle: 'long',
dateStyle: 'full'
});
// split character
const split_char = '\0;'
var chatDB = path.join(configFolder, 'chatDB')
// ---- MessageDB
var PouchDB = require('pouchdb');
var db = new PouchDB(chatDB);
// get all messages from database
//var messages = db.get("messages").value()
// get all dxcallsigns in database
var dxcallsigns = new Set();
db.allDocs({
include_docs: true,
attachments: true
}).then(function (result) {
// handle result
// get all dxcallsigns and append to list
result.rows.forEach(function(item) {
update_chat(item.doc)
});
}).catch(function (err) {
console.log(err);
});
// WINDOW LISTENER
window.addEventListener('DOMContentLoaded', () => {
// SEND MSG
document.getElementById("sendMessage").addEventListener("click", () => {
dxcallsign = document.getElementById('chatModuleDxCall').value
var dxcallsign = document.getElementById('chatModuleDxCall').value
dxcallsign = dxcallsign.toUpperCase()
message = document.getElementById('chatModuleMessage').value
console.log(dxcallsign)
let Data = {
command: "sendMessage",
dxcallsign : dxcallsign.toUpperCase(),
mode : 10,
command: "send_message",
dxcallsign : dxcallsign,
mode : 255,
frames : 1,
data : message,
checksum : '123'
};
ipcRenderer.send('run-tnc-command', Data);
ipcRenderer.send('run-tnc-command', Data);
var uuid = uuidv4();
db.post({
_id: uuid,
timestamp: Math.floor(Date.now() / 1000),
dxcallsign: dxcallsign,
dxgrid: 'NULL',
msg: message,
checksum: 'NULL',
type: "transmit"
}).then(function (response) {
// handle response
console.log("new database entry")
console.log(response)
}).catch(function (err) {
console.log(err);
});
db.get(uuid).then(function (doc) {
// handle doc
update_chat(doc)
}).catch(function (err) {
console.log(err);
});
// scroll to bottom
var element = document.getElementById("message-container");
element.scrollTo(0,element.scrollHeight);
// clear input
document.getElementById('chatModuleMessage').value = ''
})
})
ipcRenderer.on('action-update-rx-msg-buffer', (event, arg) => {
var data = arg.data
});
ipcRenderer.on('action-new-msg-received', (event, arg) => {
console.log(arg.data)
var tbl = document.getElementById("rx-msg-data");
document.getElementById("rx-msg-data").innerHTML = ''
var new_msg = arg.data
new_msg.forEach(function(item) {
console.log(item)
//for (i = 0; i < arg.data.length; i++) {
let obj = new Object();
var encoded_data = atob(item.data);
var splitted_data = encoded_data.split(split_char)
//obj.uuid = item.uuid;
item.checksum = splitted_data[2]
item.msg = splitted_data[3]
//obj.dxcallsign = item.dxcallsign;
//obj.dxgrid = item.dxgrid;
//obj.timestamp = item.timestamp;
// check if message not exists in database.
// this might cause big cpu load of file is getting too big
/*
if(!JSON.stringify(db.get("messages")).includes(item.uuid)){
console.log("new message: " + item);
db.get("messages").push(item).save();
}
*/
db.put({
_id: item.uuid,
timestamp: item.timestamp,
dxcallsign: item.dxcallsign,
dxgrid: item.dxgrid,
msg: item.msg,
checksum: item.checksum,
type: "received"
}).then(function (response) {
// handle response
console.log("new database entry")
console.log(response)
}).catch(function (err) {
console.log(err);
});
db.get(item.uuid).then(function (doc) {
// handle doc
// timestamp
update_chat(doc)
}).catch(function (err) {
console.log(err);
});
console.log("...................................")
return
});
for (i = 0; i < arg.data.length; i++) {
});
// Update chat list
update_chat = function(obj) {
var dxcallsign = obj.dxcallsign;
// CALLSIGN LIST
if(!(document.getElementById('chat-' + dxcallsign + '-list'))){
var new_callsign = `
<a class="list-group-item list-group-item-action" id="chat-${dxcallsign}-list" data-bs-toggle="list" href="#chat-${dxcallsign}" role="tab" aria-controls="chat-${dxcallsign}">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">${dxcallsign}</h5>
<!--<small>3 days ago</small>-->
</div>
<!--<p class="mb-1">JN48ea</p>-->
</a>
`;
document.getElementById('list-tab').insertAdjacentHTML("beforeend", new_callsign);
var message_area = `
<div class="tab-pane fade" id="chat-${dxcallsign}" role="tabpanel" aria-labelledby="chat-${dxcallsign}-list"></div>
`;
document.getElementById('nav-tabContent').insertAdjacentHTML("beforeend", message_area);
// create eventlistener for listening on clicking on a callsign
document.getElementById('chat-' + dxcallsign + '-list').addEventListener('click', function() {
document.getElementById('chatModuleDxCall').value = dxcallsign;
// scroll to bottom
var element = document.getElementById("message-container");
element.scrollTo(0,element.scrollHeight);
});
}
// APPEND MESSAGES TO CALLSIGN
var timestamp = dateFormat.format(obj.timestamp * 1000);
// now we update the received files list
if(!(document.getElementById('msg-' + obj._id))){
if (obj.type == 'received'){
var new_message = `
<div class="mt-3 mb-0 w-75" id="msg-${obj._id}">
<p class="font-monospace text-small mb-0 text-muted text-break">${timestamp}</p>
<div class="card border-light bg-light" id="msg-${obj._id}">
<div class="card-body">
<p class="card-text text-break text-wrap">${obj.msg}</p>
</div>
</div>
</div>
`;
}
if (obj.type == 'transmit'){
var row = document.createElement("tr");
//https://stackoverflow.com/q/51421470
//https://stackoverflow.com/a/847196
timestampRaw = arg.data[i]['TIMESTAMP']
var date = new Date(timestampRaw * 1000);
var hours = date.getHours();
var minutes = "0" + date.getMinutes();
var seconds = "0" + date.getSeconds();
var datetime = hours + ':' + minutes.substr(-2) + ':' + seconds.substr(-2);
var timestamp = document.createElement("td");
var timestampText = document.createElement('span');
timestampText.innerText = datetime
timestamp.appendChild(timestampText);
var dxCall = document.createElement("td");
var dxCallText = document.createElement('span');
dxCallText.innerText = arg.data[i]['DXCALLSIGN']
dxCall.appendChild(dxCallText);
var message = document.createElement("td");
var messageText = document.createElement('span');
var messageString = arg.data[i]['RXDATA'][0]['d'] //data
console.log(messageString)
messageText.innerText = messageString
message.appendChild(messageText);
row.appendChild(timestamp);
row.appendChild(dxCall);
row.appendChild(message);
tbl.appendChild(row);
var new_message = `
<div class="ml-auto mt-3 mb-0 w-75" style="margin-left: auto;">
<p class="font-monospace text-right mb-0 text-muted" style="text-align: right;">${timestamp}</p>
<div class="card text-right border-primary bg-primary" id="msg-${obj._id}">
<div class="card-body">
<p class="card-text text-white text-break text-wrap">${obj.msg}</p>
</div>
</div>
</div>
`;
}
var id = "chat-" + obj.dxcallsign
document.getElementById(id).insertAdjacentHTML("beforeend", new_message);
var element = document.getElementById("message-container");
element.scrollTo(0,element.scrollHeight);
}
ipcRenderer.send('run-tnc-command', {"command" : "delRxMsgBuffer"});
})
}

73
gui/preload-log.js Normal file
View file

@ -0,0 +1,73 @@
const path = require('path');
const {ipcRenderer} = require('electron');
// https://stackoverflow.com/a/26227660
var appDataFolder = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME + "/.config")
var configFolder = path.join(appDataFolder, "FreeDATA");
var configPath = path.join(configFolder, 'config.json')
const config = require(configPath);
// WINDOW LISTENER
window.addEventListener('DOMContentLoaded', () => {
// here we could add filter buttons, somewhen later..
})
ipcRenderer.on('action-update-log', (event, arg) => {
var entry = arg.entry
// remove ANSI characters from string, caused by color logging
// 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();
timestamp.appendChild(timestampText);
var logEntry = document.createElement("td");
var logEntryText = document.createElement('span');
logEntryText.innerText = entry
logEntry.appendChild(logEntryText);
row.appendChild(timestamp);
row.appendChild(logEntry);
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
window.scrollTo(0,document.body.scrollHeight);
})

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,9 @@ const {
ipcRenderer
} = require('electron')
const log = require('electron-log');
const socketLog = log.scope('tnc');
// https://stackoverflow.com/a/26227660
var appDataFolder = process.env.APPDATA || (process.platform == 'darwin' ? process.env.HOME + '/Library/Application Support' : process.env.HOME + "/.config")
var configFolder = path.join(appDataFolder, "FreeDATA");
@ -11,7 +14,10 @@ var configPath = path.join(configFolder, 'config.json')
const config = require(configPath);
var client = new net.Socket();
var msg = ''; // Current message, per connection.
var socketchunk = ''; // Current message, per connection.
// split character
const split_char = '\0;'
// globals for getting new data only if available so we are saving bandwith
var rxBufferLengthTnc = 0
@ -20,14 +26,14 @@ var rxMsgBufferLengthTnc = 0
var rxMsgBufferLengthGui = 0
// network connection Timeout
setTimeout(connectTNC, 3000)
setTimeout(connectTNC, 2000)
function connectTNC() {
//exports.connectTNC = function(){
//console.log('connecting to TNC...')
//socketLog.info('connecting to TNC...')
//clear message buffer after reconnecting or inital connection
msg = '';
socketchunk = '';
if (config.tnclocation == 'localhost') {
client.connect(3000, '127.0.0.1')
@ -37,13 +43,29 @@ function connectTNC() {
}
client.on('connect', function(data) {
console.log('TNC connection established')
socketLog.info('TNC connection established')
let Data = {
busy_state: "-",
arq_state: "-",
//channel_state: "-",
frequency: "-",
mode: "-",
bandwith: "-",
rms_level: 0
};
ipcRenderer.send('request-update-tnc-state', Data);
// also update tnc connection state
ipcRenderer.send('request-update-tnc-connection', {tnc_connection : client.readyState});
})
client.on('error', function(data) {
console.log('TNC connection error');
socketLog.error('TNC connection error');
socketLog.error(data);
let Data = {
tnc_connection: client.readyState,
busy_state: "-",
arq_state: "-",
//channel_state: "-",
@ -54,136 +76,236 @@ client.on('error', function(data) {
};
ipcRenderer.send('request-update-tnc-state', Data);
setTimeout(connectTNC, 2000)
ipcRenderer.send('request-update-tnc-connection', {tnc_connection : client.readyState});
client.destroy();
setTimeout(connectTNC, 500)
// setTimeout( function() { exports.connectTNC(tnc_host, tnc_port); }, 2000 );
});
/*
client.on('close', function(data) {
console.log(' TNC connection closed');
socketLog.info(' TNC connection closed');
setTimeout(connectTNC, 2000)
});
*/
client.on('end', function(data) {
console.log('TNC connection ended');
//setTimeout(connectTNC, 2000)
setTimeout(connectTNC, 0)
// setTimeout( function() { exports.connectTNC(tnc_host, tnc_port); }, 2000 );
socketLog.info('TNC connection ended');
ipcRenderer.send('request-update-tnc-connection', {tnc_connection : client.readyState});
client.destroy();
setTimeout(connectTNC, 500)
});
//exports.writeTncCommand = function(command){
writeTncCommand = function(command) {
//console.log(command)
//socketLog.info(command)
// we use the writingCommand function to update our TCPIP state because we are calling this function a lot
// if socket openend, we are able to run commands
if (client.readyState == 'open') {
//uiMain.setTNCconnection('open')
client.write(command + '\n');
}
if (client.readyState == 'closed') {
//uiMain.setTNCconnection('closed')
//console.log("CLOSED!!!!!")
socketLog.info("CLOSED!")
}
if (client.readyState == 'opening') {
//uiMain.setTNCconnection('opening')
//console.log("OPENING!!!!!")
console.log('connecting to TNC...')
socketLog.info('connecting to TNC...')
}
}
client.on('data', function(data) {
client.on('data', function(socketdata) {
ipcRenderer.send('request-update-tnc-connection', {tnc_connection : client.readyState});
/*
inspired by:
stackoverflow.com questions 9070700 nodejs-net-createserver-large-amount-of-data-coming-in
*/
data = data.toString('utf8'); // convert data to string
msg += data.toString('utf8'); // append data to buffer so we can stick long data together
//console.log(data)
// check if we reached an EOF, if true, clear buffer and parse JSON data
if (data.endsWith('"EOF":"EOF"}')) {
//console.log(msg)
try {
//console.log(msg)
data = JSON.parse(msg)
} catch (e) {
console.log(e); /* "SyntaxError*/
socketdata = socketdata.toString('utf8'); // convert data to string
socketchunk += socketdata// append data to buffer so we can stick long data together
// check if we received begin and end of json data
if (socketchunk.startsWith('{"') && socketchunk.endsWith('"}\n')) {
var data = ''
// split data into chunks if we received multiple commands
socketchunk = socketchunk.split("\n");
data = JSON.parse(socketchunk[0])
// search for empty entries in socketchunk and remove them
for (i = 0; i < socketchunk.length; i++) {
if (socketchunk[i] === ''){
socketchunk.splice(i, 1);
}
}
msg = '';
/* console.log("EOF detected!") */
//iterate through socketchunks array to execute multiple commands in row
for (i = 0; i < socketchunk.length; i++) {
//console.log(data)
//check if data is not empty
if(socketchunk[i].length > 0){
//try to parse JSON
try {
if (data['COMMAND'] == 'TNC_STATE') {
//console.log(data)
// set length of RX Buffer to global variable
rxBufferLengthTnc = data['RX_BUFFER_LENGTH']
rxMsgBufferLengthTnc = data['RX_MSG_BUFFER_LENGTH']
data = JSON.parse(socketchunk[i])
} catch (e) {
socketLog.info(e); // "SyntaxError
socketLog.info(socketchunk[i])
socketchunk = ''
}
}
if (data['command'] == 'tnc_state') {
//socketLog.info(data)
// set length of RX Buffer to global variable
rxBufferLengthTnc = data['rx_buffer_length']
rxMsgBufferLengthTnc = data['rx_msg_buffer_length']
let Data = {
ptt_state: data['ptt_state'],
busy_state: data['tnc_state'],
arq_state: data['arq_state'],
arq_session: data['arq_session'],
//channel_state: data['CHANNEL_STATE'],
frequency: data['frequency'],
speed_level: data['speed_level'],
mode: data['mode'],
bandwith: data['bandwith'],
rms_level: (data['audio_rms'] / 1000) * 100,
fft: data['fft'],
channel_busy: data['channel_busy'],
scatter: data['scatter'],
info: data['info'],
rx_buffer_length: data['rx_buffer_length'],
rx_msg_buffer_length: data['rx_msg_buffer_length'],
tx_n_max_retries: data['tx_n_max_retries'],
arq_tx_n_frames_per_burst: data['arq_tx_n_frames_per_burst'],
arq_tx_n_bursts: data['arq_tx_n_bursts'],
arq_tx_n_current_arq_frame: data['arq_tx_n_current_arq_frame'],
arq_tx_n_total_arq_frames: data['arq_tx_n_total_arq_frames'],
arq_rx_frame_n_bursts: data['arq_rx_frame_n_bursts'],
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_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'],
};
ipcRenderer.send('request-update-tnc-state', Data);
}
/* A TEST WITH STREAMING DATA .... */
// if we received data through network stream, we get a single data item
if (data['arq'] == 'received') {
dataArray = []
messageArray = []
socketLog.info(data)
// we need to encode here to do a deep check for checking if file or message
var encoded_data = atob(data['data'])
var splitted_data = encoded_data.split(split_char)
if(splitted_data[0] == 'f'){
dataArray.push(data)
}
if(splitted_data[0] == 'm'){
messageArray.push(data)
}
rxBufferLengthGui = dataArray.length
let Files = {
data: dataArray,
};
ipcRenderer.send('request-update-rx-buffer', Files);
rxMsgBufferLengthGui = messageArray.length
let Messages = {
data: messageArray,
};
//ipcRenderer.send('request-update-rx-msg-buffer', Messages);
ipcRenderer.send('request-new-msg-received', Messages);
}
// if we manually checking for the rx buffer we are getting an array of multiple data
if (data['command'] == 'rx_buffer') {
socketLog.info(data)
// iterate through buffer list and sort it to file or message array
dataArray = []
messageArray = []
for (i = 0; i < data['data-array'].length; i++) {
try{
// we need to encode here to do a deep check for checking if file or message
var encoded_data = atob(data['data-array'][i]['data'])
var splitted_data = encoded_data.split(split_char)
if(splitted_data[0] == 'f'){
dataArray.push(data['data-array'][i])
}
if(splitted_data[0] == 'm'){
messageArray.push(data['data-array'][i])
}
} catch (e) {
socketLog.info(e)
}
}
rxBufferLengthGui = dataArray.length
let Files = {
data: dataArray,
};
ipcRenderer.send('request-update-rx-buffer', Files);
rxMsgBufferLengthGui = messageArray.length
let Messages = {
data: messageArray,
};
//ipcRenderer.send('request-update-rx-msg-buffer', Messages);
ipcRenderer.send('request-new-msg-received', Messages);
}
let Data = {
toe: Date.now() - data['TIMESTAMP'], // time of execution
ptt_state: data['PTT_STATE'],
busy_state: data['TNC_STATE'],
arq_state: data['ARQ_STATE'],
//channel_state: data['CHANNEL_STATE'],
frequency: data['FREQUENCY'],
mode: data['MODE'],
bandwith: data['BANDWITH'],
rms_level: (data['AUDIO_RMS'] / 1000) * 100,
fft: data['FFT'],
scatter: data['SCATTER'],
info: data['INFO'],
rx_buffer_length: data['RX_BUFFER_LENGTH'],
rx_msg_buffer_length: data['RX_MSG_BUFFER_LENGTH'],
tx_n_max_retries: data['TX_N_MAX_RETRIES'],
arq_tx_n_frames_per_burst: data['ARQ_TX_N_FRAMES_PER_BURST'],
arq_tx_n_bursts: data['ARQ_TX_N_BURSTS'],
arq_tx_n_current_arq_frame: data['ARQ_TX_N_CURRENT_ARQ_FRAME'],
arq_tx_n_total_arq_frames: data['ARQ_TX_N_TOTAL_ARQ_FRAMES'],
arq_rx_frame_n_bursts: data['ARQ_RX_FRAME_N_BURSTS'],
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_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'],
};
//console.log(Data)
ipcRenderer.send('request-update-tnc-state', Data);
}
if (data['COMMAND'] == 'RX_BUFFER') {
rxBufferLengthGui = data['DATA-ARRAY'].length
let Data = {
data: data['DATA-ARRAY'],
};
ipcRenderer.send('request-update-rx-buffer', Data);
}
if (data['COMMAND'] == 'RX_MSG_BUFFER') {
rxMsgBufferLengthGui = data['DATA-ARRAY'].length
let Data = {
data: data['DATA-ARRAY'],
};
ipcRenderer.send('request-update-rx-msg-buffer', Data);
}
// check if EOF ...
//finally delete message buffer
socketchunk = '';
}
});
function hexToBytes(hex) {
@ -195,57 +317,74 @@ function hexToBytes(hex) {
//Get TNC State
exports.getTncState = function() {
command = '{"type" : "GET", "command" : "TNC_STATE", "timestamp" : ' + Date.now() + '}';
command = '{"type" : "get", "command" : "tnc_state"}';
writeTncCommand(command)
}
//Get DATA State
exports.getDataState = function() {
command = '{"type" : "GET", "command" : "DATA_STATE", "timestamp" : ' + Date.now() + '}';
command = '{"type" : "get", "command" : "data_state"}';
//writeTncCommand(command)
}
//Get Heard Stations
//exports.getHeardStations = function() {
// command = '{"type" : "GET", "command" : "HEARD_STATIONS", "timestamp" : ' + Date.now() + '}';
// writeTncCommand(command)
//}
// Send Ping
exports.sendPing = function(dxcallsign) {
command = '{"type" : "PING", "command" : "PING", "dxcallsign" : "' + dxcallsign + '", "timestamp" : ' + Date.now() + '}'
command = '{"type" : "ping", "command" : "ping", "dxcallsign" : "' + dxcallsign + '"}'
writeTncCommand(command)
}
// Send CQ
exports.sendCQ = function() {
command = '{"type" : "BROADCAST", "command" : "CQCQCQ", "timestamp" : ' + Date.now() + '}'
command = '{"type" : "broadcast", "command" : "cqcqcq"}'
writeTncCommand(command)
}
// Send File
exports.sendFile = function(dxcallsign, mode, frames, filename, filetype, data, checksum) {
command = '{"type" : "ARQ", "command" : "sendFile", "dxcallsign" : "' + dxcallsign + '", "mode" : "' + mode + '", "n_frames" : "' + frames + '", "filename" : "' + filename + '", "filetype" : "' + filetype + '", "data" : "' + data + '", "checksum" : "' + checksum + '", "timestamp" : ' + Date.now() + '}'
socketLog.info(data)
socketLog.info(filetype)
socketLog.info(filename)
var datatype = "f"
data = datatype + split_char + filename + split_char + filetype + split_char + checksum + split_char + data
socketLog.info(data)
socketLog.info(btoa(data))
data = btoa(data)
command = '{"type" : "arq", "command" : "send_raw", "parameter" : [{"dxcallsign" : "' + dxcallsign + '", "mode" : "' + mode + '", "n_frames" : "' + frames + '", "data" : "' + data + '"}]}'
writeTncCommand(command)
}
// Send Message
exports.sendMessage = function(dxcallsign, mode, frames, data, checksum) {
command = '{"type" : "ARQ", "command" : "sendMessage", "dxcallsign" : "' + dxcallsign + '", "mode" : "' + mode + '", "n_frames" : "' + frames + '", "data" : "' + data + '" , "checksum" : "' + checksum + '", "timestamp" : ' + Date.now() + '}'
console.log(command)
socketLog.info(data)
var datatype = "m"
data = datatype + split_char + split_char + checksum + split_char + data
socketLog.info(data)
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", "parameter" : [{"dxcallsign" : "' + dxcallsign + '", "mode" : "' + mode + '", "n_frames" : "' + frames + '", "data" : "' + data + '"}]}'
socketLog.info(command)
socketLog.info("-------------------------------------")
writeTncCommand(command)
}
//STOP TRANSMISSION
exports.stopTransmission = function() {
command = '{"type" : "ARQ", "command": "stopTransmission", "timestamp" : ' + Date.now() + '}'
command = '{"type" : "arq", "command": "stop_transmission"}'
writeTncCommand(command)
}
// Get RX BUffer
exports.getRxBuffer = function() {
command = '{"type" : "GET", "command" : "RX_BUFFER", "timestamp" : ' + Date.now() + '}'
command = '{"type" : "get", "command" : "rx_buffer"}'
// call command only if new data arrived
if (rxBufferLengthGui != rxBufferLengthTnc) {
@ -253,35 +392,27 @@ exports.getRxBuffer = function() {
}
}
// Get RX MSG BUffer
exports.getMsgRxBuffer = function() {
command = '{"type" : "GET", "command" : "RX_MSG_BUFFER", "timestamp" : ' + Date.now() + '}'
// call command only if new data arrived
if (rxMsgBufferLengthGui != rxMsgBufferLengthTnc) {
writeTncCommand(command)
}
}
// DELETE RX MSG BUffer
exports.delRxMsgBuffer = function() {
command = '{"type" : "SET", "command" : "DEL_RX_MSG_BUFFER", "timestamp" : ' + Date.now() + '}'
// call command only if new data arrived
if (rxMsgBufferLengthGui != rxMsgBufferLengthTnc) {
writeTncCommand(command)
}
}
// START BEACON
exports.startBeacon = function(interval) {
command = '{"type" : "BROADCAST", "command" : "START_BEACON", "parameter": "' + interval + '","timestamp" : ' + Date.now() + '}'
command = '{"type" : "broadcast", "command" : "start_beacon", "parameter": "' + interval + '"}'
writeTncCommand(command)
}
// STOP BEACON
exports.stopBeacon = function() {
command = '{"type" : "BROADCAST", "command" : "STOP_BEACON", "timestamp" : ' + Date.now() + '}'
command = '{"type" : "broadcast", "command" : "stop_beacon"}'
writeTncCommand(command)
}
// OPEN ARQ SESSION
exports.connectARQ = function(dxcallsign) {
command = '{"type" : "arq", "command" : "connect", "dxcallsign": "'+ dxcallsign + '"}'
writeTncCommand(command)
}
// CLOSE ARQ SESSION
exports.disconnectARQ = function() {
command = '{"type" : "arq", "command" : "disconnect"}'
writeTncCommand(command)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

View file

@ -7,7 +7,9 @@
<meta http-equiv="Content-Security-Policy" content="script-src 'self';">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="../node_modules/bootstrap-icons/font/bootstrap-icons.css">
<!-- Custom CSS -->
<link rel="stylesheet" type="text/css" href="styles.css" />
<title>FreeDATA - CHAT</title>
</head>
<body>
@ -18,36 +20,61 @@
<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>
<div class="input-group m-1">
<input class="form-control" maxlength="6" style="max-width: 6rem; text-transform:uppercase" id="chatModuleDxCall" placeholder="DX CALL"></input>
<input class="form-control" id="chatModuleMessage" placeholder="Message"></input>
<button class="btn btn-sm btn-primary me-2" id="sendMessage" type="button">SEND MSG</button>
<div class="container-fluid">
<div class="row h-100">
<div class="col-4 p-0">
<! ------Chats area ---------------------------------------------------------------------->
<div class="list-group rounded-0" id="list-tab" role="tablist">
</div>
</div>
<div class="col-8 border vh-100 ">
<! ------messages area ---------------------------------------------------------------------->
<div class="container overflow-auto" id="message-container" style="height: 90%">
<div class="tab-content" id="nav-tabContent">
</div>
<!--<div class="container position-absolute bottom-0">-->
</div>
<!-- </div>-->
</div>
</div>
<nav class="navbar fixed-bottom navbar-light bg-light">
<div class="container-fluid">
<div class="input-group bottom-0 w-100">
<input class="form-control" maxlength="8" style="max-width: 6rem; text-transform:uppercase" id="chatModuleDxCall" placeholder="DX CALL"></input>
<input class="form-control" id="chatModuleMessage" placeholder="Message"></input>
<button class="btn btn-sm btn-primary me-2" id="sendMessage" type="button"><i class="bi bi-send" style="font-size: 1.2rem; color: white;"></i></button>
</div>
</div>
</nav>
</div>
<table class="table">
<thead>
<tr>
<th scope="col">Time</th>
<th scope="col">DXCall</th>
<!--<th scope="col">DXGrid</th>
<th scope="col">Distance</th>-->
<th scope="col">Message</th>
<!--<th scope="col">SNR</th>-->
</tr>
</thead>
<tbody id="rx-msg-data">
<!--
<tr>
<th scope="row">1</th>
<td>Mark</td>
<td>Otto</td>
<td>@mdo</td>
</tr>
-->
</tbody>
</table>
</body>
</html>

View file

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 110 KiB

BIN
gui/src/img/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 KiB

View file

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

View file

@ -8,89 +8,151 @@
<meta http-equiv="Content-Security-Policy" content="script-src 'self';">
<!-- <meta http-equiv="Content-Security-Policy" content="script-src 'self';">-->
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="../node_modules/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" id="bootstrap_theme" href="../node_modules/bootstrap/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="../node_modules/bootstrap-icons/font/bootstrap-icons.css">
<!-- Waterfall CSS -->
<link rel="stylesheet" type="text/css" href="waterfall/waterfall.css" />
<link rel="stylesheet" type="text/css" href="styles.css" />
<title>FreeDATA</title>
<title>FreeDATA by DJ2LS</title>
</head>
<body>
<!-- SECONDARY NAVBAR -->
<nav class="navbar bg-light fixed-top navbar-underline mt-0 mb-1 pb-1 pt-1 shadow">
<nav class="navbar bg-light fixed-top mt-0 mb-1 pb-1 pt-1 shadow-sm">
<div class="container-fluid mt-0">
<div class="btn-toolbar" role="toolbar" aria-label="Toolbar with button groups">
<div class="btn-group btn-group-sm me-2" role="group" aria-label="local-remote-switch toggle button group" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="true" title="Select a local or a remote location of your tnc daemon. Normally local is the preferred option.">
<input type="radio" class="btn-check" name="local-remote-switch" id="local-remote-switch1" autocomplete="off" checked>
<label class="btn btn-outline-secondary" for="local-remote-switch1">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-pc-display-horizontal" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1.5 0A1.5 1.5 0 0 0 0 1.5v7A1.5 1.5 0 0 0 1.5 10H6v1H1a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1h-5v-1h4.5A1.5 1.5 0 0 0 16 8.5v-7A1.5 1.5 0 0 0 14.5 0h-13Zm0 1a.5.5 0 0 0-.5.5v7a.5.5 0 0 0 .5.5h13a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.5-.5h-13ZM12 12.5a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm2 0a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0ZM1.5 12a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5ZM1 14.25a.25.25 0 0 1 .25-.25h5.5a.25.25 0 1 1 0 .5h-5.5a.25.25 0 0 1-.25-.25Z" />
</svg>
<label class="btn btn-sm btn-outline-secondary" for="local-remote-switch1">
<i class="bi bi-pc-display-horizontal" style="font-size: 1rem; color: black;"></i>
</label>
<input type="radio" class="btn-check" name="local-remote-switch" id="local-remote-switch2" autocomplete="off">
<label class="btn btn-outline-secondary" for="local-remote-switch2">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-ethernet" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2ZM1 2a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2Zm13 11.5v-7a.5.5 0 0 0-.5-.5H12V4.5a.5.5 0 0 0-.5-.5h-1v-.5A.5.5 0 0 0 10 3H6a.5.5 0 0 0-.5.5V4h-1a.5.5 0 0 0-.5.5V6H2.5a.5.5 0 0 0-.5.5v7a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5ZM3.75 11a.25.25 0 0 0-.25.25v1.5c0 .138.112.25.25.25h.5a.25.25 0 0 0 .25-.25v-1.5a.25.25 0 0 0-.25-.25h-.5Zm2 0a.25.25 0 0 0-.25.25v1.5c0 .138.112.25.25.25h.5a.25.25 0 0 0 .25-.25v-1.5a.25.25 0 0 0-.25-.25h-.5Zm1.75.25a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v1.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-1.5ZM9.75 11a.25.25 0 0 0-.25.25v1.5c0 .138.112.25.25.25h.5a.25.25 0 0 0 .25-.25v-1.5a.25.25 0 0 0-.25-.25h-.5Zm1.75.25a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v1.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-1.5Z" />
</svg>
<label class="btn btn-sm btn-outline-secondary" for="local-remote-switch2">
<i class="bi bi-ethernet" style="font-size: 1rem; color: black;"></i>
</label>
</div>
<div class="input-group input-group-sm me-2" id="remote-tnc-field"> <span class="input-group-text" id="basic-addon1">IP</span>
<div class="input-group input-group-sm me-2" id="remote-tnc-field"> <span class="input-group-text" id="basic-addon1">TNC IP</span>
<input type="text" class="form-control" placeholder="ip adress" id="tnc_adress" value="192.168.178.163" maxlength="17" style="width: 8rem" aria-label="Username" aria-describedby="basic-addon1"> <span class="input-group-text" id="basic-addon1">:</span>
<input type="text" class="form-control" placeholder="port" value="3000" id="tnc_port" maxlength="5" max="65534" min="1025" style="width: 4rem" aria-label="Username" aria-describedby="basic-addon1">
<button class="btn btn-sm btn-danger" id="daemon_connection_state" type="button" disabled>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-diagram-3" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M6 3.5A1.5 1.5 0 0 1 7.5 2h1A1.5 1.5 0 0 1 10 3.5v1A1.5 1.5 0 0 1 8.5 6v1H14a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-1 0V8h-5v.5a.5.5 0 0 1-1 0V8h-5v.5a.5.5 0 0 1-1 0v-1A.5.5 0 0 1 2 7h5.5V6A1.5 1.5 0 0 1 6 4.5v-1zM8.5 5a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1zM0 11.5A1.5 1.5 0 0 1 1.5 10h1A1.5 1.5 0 0 1 4 11.5v1A1.5 1.5 0 0 1 2.5 14h-1A1.5 1.5 0 0 1 0 12.5v-1zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1zm4.5.5A1.5 1.5 0 0 1 7.5 10h1a1.5 1.5 0 0 1 1.5 1.5v1A1.5 1.5 0 0 1 8.5 14h-1A1.5 1.5 0 0 1 6 12.5v-1zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1zm4.5.5a1.5 1.5 0 0 1 1.5-1.5h1a1.5 1.5 0 0 1 1.5 1.5v1a1.5 1.5 0 0 1-1.5 1.5h-1a1.5 1.5 0 0 1-1.5-1.5v-1zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1z" />
</svg>
<i class="bi bi-diagram-3" style="font-size: 1rem; color: white;"></i>
</button>
</div>
<div class="input-group input-group-sm" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="true" title="Start or stop the tnc process. Please set your audio and radio settings first!">
<button type="button" id="startTNC" class="btn btn-success">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-play-fill" viewBox="0 0 16 16">
<path d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z" />
</svg>
<button type="button" id="startTNC" class="btn btn-sm btn-success">
<i class="bi bi-play-fill" style="font-size: 1rem; color: white;"></i>
</button>
<!-- TNC LOG BUTTON -->
<button class="btn btn-sm btn-secondary" id="tncLog" type="button">
<i class="bi bi-activity" style="font-size: 1rem; color: white;"></i>
</button>
<div class="input-group-text">TNC</div>
<!-- <span class="input-group-text" id="tnc_running_state" style="width: 5rem">---</span>-->
<button type="button" data-bs-toggle="collapse" data-bs-target=".multi-collapse" id="stopTNC" class="btn btn-sm btn-danger">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-x-octagon-fill" viewBox="0 0 16 16">
<path d="M11.46.146A.5.5 0 0 0 11.107 0H4.893a.5.5 0 0 0-.353.146L.146 4.54A.5.5 0 0 0 0 4.893v6.214a.5.5 0 0 0 .146.353l4.394 4.394a.5.5 0 0 0 .353.146h6.214a.5.5 0 0 0 .353-.146l4.394-4.394a.5.5 0 0 0 .146-.353V4.893a.5.5 0 0 0-.146-.353L11.46.146zm-6.106 4.5L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 1 1 .708-.708z" />
</svg>
<i class="bi bi-x-octagon-fill" style="font-size: 1rem; color: white;"></i>
</button>
</div>
</div>
<div class="btn-toolbar" role="toolbar">
<button class="btn btn-sm btn-primary me-2" 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>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-chat-left-text-fill" viewBox="0 0 16 16">
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4.414a1 1 0 0 0-.707.293L.854 15.146A.5.5 0 0 1 0 14.793V2zm3.5 1a.5.5 0 0 0 0 1h9a.5.5 0 0 0 0-1h-9zm0 2.5a.5.5 0 0 0 0 1h9a.5.5 0 0 0 0-1h-9zm0 2.5a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5z" />
</svg>
</button> <span data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="View the received files. This is currently under development!">
<button class="btn btn-sm btn-primary me-4 position-relative" id="openRFChat" type="button" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="Open the HF chat module. This is currently just a test and not finished, yet!" disabled> <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">soon...</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>Received Files </strong>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-file-earmark-arrow-down-fill" viewBox="0 0 16 16">
<path d="M9.293 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.707A1 1 0 0 0 13.707 4L10 .293A1 1 0 0 0 9.293 0zM9.5 3.5v-2l3 3h-2a1 1 0 0 1-1-1zm-1 4v3.793l1.146-1.147a.5.5 0 0 1 .708.708l-2 2a.5.5 0 0 1-.708 0l-2-2a.5.5 0 0 1 .708-.708L7.5 11.293V7.5a.5.5 0 0 1 1 0z" />
</svg>
<i class="bi bi-file-earmark-arrow-down-fill" style="font-size: 1rem; color: white;"></i>
</button>
</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" > <strong>Transmit File </strong>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-file-earmark-arrow-up-fill" viewBox="0 0 16 16">
<path d="M9.293 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.707A1 1 0 0 0 13.707 4L10 .293A1 1 0 0 0 9.293 0zM9.5 3.5v-2l3 3h-2a1 1 0 0 1-1-1zM6.354 9.854a.5.5 0 0 1-.708-.708l2-2a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1-.708.708L8.5 8.707V12.5a.5.5 0 0 1-1 0V8.707L6.354 9.854z" />
</svg>
</button>
<i class="bi bi-file-earmark-arrow-up-fill" style="font-size: 1rem; color: white;"></i>
</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">
<i class="bi bi-sliders" style="font-size: 1rem; color: white;"></i>
</button>
</span>
</div>
</div>
</nav>
<div id="blurdiv" style="-webkit-Filter: blur(10px)">
<div id="blurdiv" style="-webkit-Filter: blur(0px)">
<!--beginn of blur div -->
<!-------------------------------- MAIN AREA ---------------->
<!-------------------------------- UPDATE TOASTS ---------------->
<div aria-live="polite" aria-atomic="true" class="position-relative" style="z-index: 500">
<div class="toast-container position-absolute top-0 start-0 end-0 p-3">
<!-- CHECKING FOR UPDATE -->
<div class="toast align-items-center text-white bg-secondary border-0" id="toastUpdateChecking" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body"><i class="bi bi-search" style="font-size: 1rem; color: white;"></i> Checking for update</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
<!-- DOWNLOAD PROGRESS -->
<div class="toast align-items-center bg-white" id="toastUpdateProgress" role="alert" aria-live="assertive" aria-atomic="true" data-bs-animation="false" data-bs-autohide="false">
<!--<div class="d-flex"> -->
<div class="toast-header"> <strong class="me-auto">Downloading...</strong>
<small><span id="toastUpdateProgressSpeed"></span></small>
</div>
<div class="toast-body w-100">
<div class="progress">
<div class="progress-bar" style="width: 75%" role="progressbar" id="toastUpdateProgressBar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"> <span id="toastUpdateProgressInfo"></span>
</div>
</div>
</div>
</div>
<!-- DOWNLOADED -->
<div class="toast bg-white" role="alert" data-bs-autohide="false" id="toastUpdateDownloaded" aria-live="assertive" aria-atomic="true">
<div class="toast-header"> <strong class="me-auto"> Update ready...</strong>
</div>
<div class="toast-body">App is going to restart automatically...</div>
</div>
<!-- UPDATE AVAILABLE -->
<div class="toast align-items-center text-white bg-primary border-0" data-bs-autohide="false" id="toastUpdateAvailable" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body"> <i class="bi bi-activity" style="font-size: 1rem; color: white;"></i> Preparing for download...</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
<!-- UPDATE NOT AVAILABLE -->
<div class="toast align-items-center text-white bg-success border-0" id="toastUpdateNotAvailable" role="alert" aria-live="assertive" aria-atomic="true"data-bs-delay="1500">
<div class="d-flex">
<div class="toast-body"> <i class="bi bi-activity" style="font-size: 1rem; color: white;"></i> Up to date!</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
<!-- NOT CHECKING FOR UPDATE -->
<div class="toast align-items-center text-white bg-warning border-0" id="toastUpdateNotChecking" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">Update not available - Please try again later!</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
</div>
</div>
<!-------------------------------- INFO TOASTS ---------------->
<div aria-live="polite" aria-atomic="true" class="position-relative" style="z-index: 500">
<div class="toast-container position-absolute top-0 end-0 p-3">
<!-- SENDING CQ -->
<div class="toast align-items-center text-white bg-primary border-0" id="toastCQsending" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">Sending CQ CQ CQ</div>
<div class="toast-body"><i class="bi bi-activity" style="font-size: 1rem; color: white;"></i>Sending CQ CQ CQ</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
<!-- RECEIVING CQ -->
@ -213,29 +275,18 @@
<div class="col">
<div class="card text-dark mb-0">
<div class="card-header p-1">
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" fill="currentColor" class="bi bi-volume-up" viewBox="0 0 16 16">
<path d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z" />
<path d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z" />
<path d="M10.025 8a4.486 4.486 0 0 1-1.318 3.182L8 10.475A3.489 3.489 0 0 0 9.025 8c0-.966-.392-1.841-1.025-2.475l.707-.707A4.486 4.486 0 0 1 10.025 8zM7 4a.5.5 0 0 0-.812-.39L3.825 5.5H1.5A.5.5 0 0 0 1 6v4a.5.5 0 0 0 .5.5h2.325l2.363 1.89A.5.5 0 0 0 7 12V4zM4.312 6.39 6 5.04v5.92L4.312 9.61A.5.5 0 0 0 4 9.5H2v-3h2a.5.5 0 0 0 .312-.11z" />
</svg> <strong>AUDIO SETTINGS</strong>
<i class="bi bi-volume-up" style="font-size: 1rem; color: black;"></i><strong>AUDIO</strong>
</div>
<div class="card-body p-2 mb-1">
<div class="input-group input-group-sm mb-1"> <span class="input-group-text" id="basic-addon1">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-mic-fill" viewBox="0 0 16 16">
<path d="M5 3a3 3 0 0 1 6 0v5a3 3 0 0 1-6 0V3z"/>
<path d="M3.5 6.5A.5.5 0 0 1 4 7v1a4 4 0 0 0 8 0V7a.5.5 0 0 1 1 0v1a5 5 0 0 1-4.5 4.975V15h3a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1h3v-2.025A5 5 0 0 1 3 8V7a.5.5 0 0 1 .5-.5z"/>
</svg>
<i class="bi bi-mic-fill" style="font-size: 1rem; color: black;"></i>
</span>
<select class="form-select form-select-sm" id="audio_input_selectbox" aria-label=".form-select-sm">
<!-- <option selected value="3011">USB Interface</option>-->
</select>
</div>
<div class="input-group input-group-sm mb-1"> <span class="input-group-text" id="basic-addon1">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-volume-up-fill" viewBox="0 0 16 16">
<path d="M11.536 14.01A8.473 8.473 0 0 0 14.026 8a8.473 8.473 0 0 0-2.49-6.01l-.708.707A7.476 7.476 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303l.708.707z"/>
<path d="M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.483 5.483 0 0 1 11.025 8a5.483 5.483 0 0 1-1.61 3.89l.706.706z"/>
<path d="M8.707 11.182A4.486 4.486 0 0 0 10.025 8a4.486 4.486 0 0 0-1.318-3.182L8 5.525A3.489 3.489 0 0 1 9.025 8 3.49 3.49 0 0 1 8 10.475l.707.707zM6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06z"/>
</svg>
<i class="bi bi-volume-up" style="font-size: 1rem; color: black;"></i>
</span>
<select class="form-select form-select-sm" id="audio_output_selectbox" aria-label=".form-select-sm"></select>
</div>
@ -246,26 +297,29 @@
<div class="col">
<div class="card text-dark mb-0">
<div class="card-header p-1">
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" fill="currentColor" class="bi bi-projector" viewBox="0 0 16 16">
<path d="M14 7.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM2.5 6a.5.5 0 0 0 0 1h4a.5.5 0 0 0 0-1h-4Zm0 2a.5.5 0 0 0 0 1h4a.5.5 0 0 0 0-1h-4Z" />
<path fill-rule="evenodd" d="M0 6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2 1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1H5a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1 2 2 0 0 1-2-2V6Zm2-1h12a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Z" />
</svg> <strong>RADIO SETTINGS</strong>
<i class="bi bi-projector" style="font-size: 1rem; color: black;"></i><strong>RADIO</strong>
<div class="btn-group btn-group-sm" role="group" aria-label="waterfall-scatter-switch toggle button group">
<input type="radio" class="btn-check" name="radio-control-switch" id="radio-control-switch0" autocomplete="off" checked>
<label class="btn btn-sm btn-outline-secondary" for="radio-control-switch0">
<i class="bi bi-x-circle" style="font-size: 0.8rem; color: black;"></i>
</label>
<input type="radio" class="btn-check" name="radio-control-switch" id="radio-control-switch1" autocomplete="off" checked>
<label class="btn btn-sm btn-outline-secondary" for="radio-control-switch1"><strong>direct</strong>
</label>
<!--
<input type="radio" class="btn-check" name="radio-control-switch" id="radio-control-switch2" autocomplete="off">
<label class="btn btn-sm btn-outline-secondary" for="radio-control-switch2"><strong>rigctl</strong>
</label>
-->
<input type="radio" class="btn-check" name="radio-control-switch" id="radio-control-switch3" autocomplete="off">
<label class="btn btn-sm btn-outline-secondary" for="radio-control-switch3"><strong>rigctld</strong>
</label>
</div>
<div class="btn-group" role="group" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="true" title="Set advanced hamlib settings like stop_bits and data_bits or a different port for a ptt device">
<button type="button" id="advancedHamlibSettingsButton" data-bs-toggle="modal" data-bs-target="#advancedHamlibSettingsModal" class="btn btn-sm btn-secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-sliders" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M11.5 2a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM9.05 3a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0V3h9.05zM4.5 7a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zM2.05 8a2.5 2.5 0 0 1 4.9 0H16v1H6.95a2.5 2.5 0 0 1-4.9 0H0V8h2.05zm9.45 4a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm-2.45 1a2.5 2.5 0 0 1 4.9 0H16v1h-2.05a2.5 2.5 0 0 1-4.9 0H0v-1h9.05z" />
</svg>
<i class="bi bi-sliders" style="font-size: 0.8rem; color: black;"></i>
</button>
</div>
<div class="btn-group" role="group">
@ -275,11 +329,9 @@
<div class="card-body p-2">
<div id="radio-control-direct">
<div class="input-group input-group-sm mb-1"> <span class="input-group-text" id="basic-addon1">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-projector-fill" viewBox="0 0 16 16">
<path d="M2 4a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2 1 1 0 0 0 1 1h1a1 1 0 0 0 1-1h6a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1 2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H2Zm.5 2h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1 0-1ZM14 7.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm-12 1a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5Z"/>
</svg>
<i class="bi bi-projector-fill" style="font-size: 0.8rem; color: black;"></i>
</span>
<!--<select class="form-select form-select-sm" aria-label=".form-select-sm" id="hamlib_deviceid" style="width:7rem">-->
<input class="form-control" list="datalistOptions" id="hamlib_deviceid" placeholder="Search radio..." style="width:7rem">
<datalist id="datalistOptions">
<option value="RIG_MODEL_ADT_200A">ADAT www.adat.ch ADT-200A</option>
@ -546,12 +598,11 @@
<option value="RIG_MODEL_FTDX101MP">Yaesu FTDX-101MP</option>
</datalist> <span class="input-group-text" id="basic-addon1">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-alt" viewBox="0 0 16 16">
<path d="M1 13.5a.5.5 0 0 0 .5.5h3.797a.5.5 0 0 0 .439-.26L11 3h3.5a.5.5 0 0 0 0-1h-3.797a.5.5 0 0 0-.439.26L5 13H1.5a.5.5 0 0 0-.5.5zm10 0a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 0-1h-3a.5.5 0 0 0-.5.5z"/>
</svg>
<i class="bi bi-alt" style="font-size: 1rem; color: black;"></i>
</span>
<select class="form-select form-select-sm" aria-label=".form-select-sm" id="hamlib_ptt_protocol" style="width: 0.5rem">
<option value="NONE">NONE</option>
<option value="RIG">RIG</option>
<option value="USB">USB</option>
<option value="RTS">Serial RTS</option>
@ -565,9 +616,7 @@
<!--<hr class="m-1">-->
<div class="input-group input-group-sm mb-1"> <span class="input-group-text" id="basic-addon1">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-usb-symbol" viewBox="0 0 16 16">
<path d="m7.792.312-1.533 2.3A.25.25 0 0 0 6.467 3H7.5v7.319a2.5 2.5 0 0 0-.515-.298L5.909 9.56A1.5 1.5 0 0 1 5 8.18v-.266a1.5 1.5 0 1 0-1 0v.266a2.5 2.5 0 0 0 1.515 2.298l1.076.461a1.5 1.5 0 0 1 .888 1.129 2.001 2.001 0 1 0 1.021-.006v-.902a1.5 1.5 0 0 1 .756-1.303l1.484-.848A2.5 2.5 0 0 0 11.995 7h.755a.25.25 0 0 0 .25-.25v-2.5a.25.25 0 0 0-.25-.25h-2.5a.25.25 0 0 0-.25.25v2.5c0 .138.112.25.25.25h.741a1.5 1.5 0 0 1-.747 1.142L8.76 8.99a2.584 2.584 0 0 0-.26.17V3h1.033a.25.25 0 0 0 .208-.389L8.208.312a.25.25 0 0 0-.416 0Z"/>
</svg>
<i class="bi bi-usb-symbol" style="font-size: 1rem; color: black;"></i>
</span>
<select class="form-select form-select-sm" aria-label=".form-select-sm" id="hamlib_deviceport" style="width:7rem">
@ -588,24 +637,22 @@
</select>
</div>
</div>
<!--
<div id="radio-control-rigctl">
<div class="input-group input-group-sm mb-1"> <span class="input-group-text" id="basic-addon1">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-projector-fill" viewBox="0 0 16 16">
<path d="M2 4a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2 1 1 0 0 0 1 1h1a1 1 0 0 0 1-1h6a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1 2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H2Zm.5 2h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1 0-1ZM14 7.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm-12 1a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5Z"/>
</svg>
<i class="bi bi-projector" style="font-size: 1rem; color: black;"></i>
</span>
<input type="text" class="form-control" placeholder="Device id" id="hamlib_deviceid_rigctl" aria-label="Device ID" aria-describedby="basic-addon1"> <span class="input-group-text" id="basic-addon1">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-alt" viewBox="0 0 16 16">
<path d="M1 13.5a.5.5 0 0 0 .5.5h3.797a.5.5 0 0 0 .439-.26L11 3h3.5a.5.5 0 0 0 0-1h-3.797a.5.5 0 0 0-.439.26L5 13H1.5a.5.5 0 0 0-.5.5zm10 0a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 0-1h-3a.5.5 0 0 0-.5.5z"/>
</svg>
<i class="bi bi-alt" style="font-size: 1rem; color: black;"></i>
</span>
<select class="form-select form-select-sm" aria-label=".form-select-sm" id="hamlib_ptt_protocol_rigctl" style="width: 0.5rem">
<option value="RIG">RIG</option>
<option value="USB">USB</option>
<option value="RTS">Serial RTS</option>
<option value="DTR-H">Serial DTR-High</option>
<option value="DTR-H">Serial DTR-Low</option>
<option value="DTR-L">Serial DTR-Low</option>
<option value="PARALLEL">Rig PARALLEL</option>
<option value="MICDATA">Rig MICDATA</option>
<option value="CM108">Rig CM108</option>
@ -613,14 +660,11 @@
</div>
<div class="input-group input-group-sm mb-1"> <span class="input-group-text" id="basic-addon1">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-usb-symbol" viewBox="0 0 16 16">
<path d="m7.792.312-1.533 2.3A.25.25 0 0 0 6.467 3H7.5v7.319a2.5 2.5 0 0 0-.515-.298L5.909 9.56A1.5 1.5 0 0 1 5 8.18v-.266a1.5 1.5 0 1 0-1 0v.266a2.5 2.5 0 0 0 1.515 2.298l1.076.461a1.5 1.5 0 0 1 .888 1.129 2.001 2.001 0 1 0 1.021-.006v-.902a1.5 1.5 0 0 1 .756-1.303l1.484-.848A2.5 2.5 0 0 0 11.995 7h.755a.25.25 0 0 0 .25-.25v-2.5a.25.25 0 0 0-.25-.25h-2.5a.25.25 0 0 0-.25.25v2.5c0 .138.112.25.25.25h.741a1.5 1.5 0 0 1-.747 1.142L8.76 8.99a2.584 2.584 0 0 0-.26.17V3h1.033a.25.25 0 0 0 .208-.389L8.208.312a.25.25 0 0 0-.416 0Z"/>
</svg>
<i class="bi bi-usb-symbol" style="font-size: 1rem; color: black;"></i>
</span>
<select class="form-select form-select-sm" aria-label=".form-select-sm" id="hamlib_deviceport_rigctl" style="width:7rem">
<!--<option selected value="/dev/ttyUSB0">/dev/ttyUSB0</option>
<option value="/dev/ttyUSB1">/dev/ttyUSB1</option>-->
</select> <span class="input-group-text" id="basic-addon1">Speed</span>
<select class="form-select form-select-sm" aria-label=".form-select-sm" id="hamlib_serialspeed_rigctl">
<option value="1200">1200</option>
@ -636,11 +680,11 @@
</select>
</div>
</div>
-->
<div id="radio-control-rigctld">
<div class="input-group input-group-sm mb-1">
<input type="text" class="form-control" placeholder="rigctld IP" id="hamlib_rigctld_ip" aria-label="Device IP" aria-describedby="basic-addon1">
<input type="text" class="form-control" placeholder="rigctld port" id="hamlib_rigctld_port" aria-label="Device Port" aria-describedby="basic-addon1">
<div class="input-group input-group-sm mb-1"> <span class="input-group-text" id="basic-addon1">IP</span>
<input type="text" class="form-control" placeholder="rigctld IP" id="hamlib_rigctld_ip" aria-label="Device IP" aria-describedby="basic-addon1"> <span class="input-group-text" id="basic-addon1">:</span>
<input type="text" class="form-control" placeholder="rigctld port" id="hamlib_rigctld_port" aria-label="Device Port" aria-describedby="basic-addon1">
</div>
</div>
</div>
@ -657,31 +701,43 @@
<div class="row">
<div class="col-md-auto">
<div class="input-group input-group-sm mb-0" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="Enter your callsign and save it"> <span class="input-group-text" id="basic-addon1">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-person-bounding-box" viewBox="0 0 16 16">
<path d="M1.5 1a.5.5 0 0 0-.5.5v3a.5.5 0 0 1-1 0v-3A1.5 1.5 0 0 1 1.5 0h3a.5.5 0 0 1 0 1h-3zM11 .5a.5.5 0 0 1 .5-.5h3A1.5 1.5 0 0 1 16 1.5v3a.5.5 0 0 1-1 0v-3a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 1-.5-.5zM.5 11a.5.5 0 0 1 .5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 1 0 1h-3A1.5 1.5 0 0 1 0 14.5v-3a.5.5 0 0 1 .5-.5zm15 0a.5.5 0 0 1 .5.5v3a1.5 1.5 0 0 1-1.5 1.5h-3a.5.5 0 0 1 0-1h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 1 .5-.5z"/>
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm8-9a3 3 0 1 1-6 0 3 3 0 0 1 6 0z"/>
</svg>
<i class="bi bi-person-bounding-box" style="font-size: 1rem; color: black;"></i>
</span>
<input type="text" class="form-control" style="max-width: 6rem; text-transform:uppercase" placeholder="callsign" pattern="[A-Z]*" id="myCall" maxlength="6" aria-label="Input group" aria-describedby="btnGroupAddon">
<button class="btn btn-success" id="saveMyCall" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-check2" viewBox="0 0 16 16">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
</svg>
<input type="text" class="form-control" style="width: 5rem; text-transform:uppercase" placeholder="callsign" pattern="[A-Z]*" id="myCall" maxlength="8" aria-label="Input group" aria-describedby="btnGroupAddon">
<select class="form-select form-select-sm" aria-label=".form-select-sm" id="myCallSSID">
<option selected value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
<option value="13">13</option>
<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">
<div class="input-group input-group-sm mb-0" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="Enter your gridsquare and save it"> <span class="input-group-text" id="basic-addon1">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-house-fill" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="m8 3.293 6 6V13.5a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 13.5V9.293l6-6zm5-.793V6l-2-2V2.5a.5.5 0 0 1 .5-.5h1a.5.5 0 0 1 .5.5z"/>
<path fill-rule="evenodd" d="M7.293 1.5a1 1 0 0 1 1.414 0l6.647 6.646a.5.5 0 0 1-.708.708L8 2.207 1.354 8.854a.5.5 0 1 1-.708-.708L7.293 1.5z"/>
</svg>
<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-success" id="saveMyGrid" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-check2" viewBox="0 0 16 16">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" />
</svg>
<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>
@ -693,17 +749,26 @@
<div class="col">
<div class="card text-dark mb-0">
<div class="card-header p-1">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-motherboard" viewBox="0 0 16 16">
<path d="M11.5 2a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5Zm2 0a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5Zm-10 8a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6Zm0 2a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6ZM5 3a1 1 0 0 0-1 1h-.5a.5.5 0 0 0 0 1H4v1h-.5a.5.5 0 0 0 0 1H4a1 1 0 0 0 1 1v.5a.5.5 0 0 0 1 0V8h1v.5a.5.5 0 0 0 1 0V8a1 1 0 0 0 1-1h.5a.5.5 0 0 0 0-1H9V5h.5a.5.5 0 0 0 0-1H9a1 1 0 0 0-1-1v-.5a.5.5 0 0 0-1 0V3H6v-.5a.5.5 0 0 0-1 0V3Zm0 1h3v3H5V4Zm6.5 7a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h2a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-2Z" />
<path d="M1 2a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-2H.5a.5.5 0 0 1-.5-.5v-1A.5.5 0 0 1 .5 9H1V8H.5a.5.5 0 0 1-.5-.5v-1A.5.5 0 0 1 .5 6H1V5H.5a.5.5 0 0 1-.5-.5v-2A.5.5 0 0 1 .5 2H1Zm1 11a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v11Z" />
</svg> <strong>SYSTEM STATUS</strong>
<i class="bi bi-sliders2" style="font-size: 1rem; color: black;"></i> <strong>TNC SETTINGS</strong>
</div>
<div class="card-body p-2 mb-1">
<button class="btn btn-secondary btn-sm" id="python_version" type="button" disabled>Python</button>
<button class="btn btn-secondary btn-sm" id="node_version" type="button" disabled>Node</button>
<button class="btn btn-secondary btn-sm" id="hamlib_version" type="button" disabled>Hamlib</button>
<div class="form-check form-switch form-check-inline">
<input class="form-check-input" type="checkbox" id="fftSwitch">
<label class="form-check-label" for="fftSwitch">Waterfall</label>
</div>
<div class="form-check form-switch form-check-inline">
<input class="form-check-input" type="checkbox" id="scatterSwitch">
<label class="form-check-label" for="scatterSwitch">Scatter</label>
</div>
<div class="form-check form-switch form-check-inline">
<input class="form-check-input" type="checkbox" id="500HzModeSwitch">
<label class="form-check-label" for="500HzModeSwitch">500Hz</label>
</div>
<!--<button class="btn btn-secondary btn-sm" id="python_version" type="button" disabled>Python</button>-->
<!--<button class="btn btn-secondary btn-sm" id="node_version" type="button" disabled>Node</button>-->
<!--<button class="btn btn-secondary btn-sm" id="hamlib_version" type="button" disabled>Hamlib</button>-->
<!--<button class="btn btn-secondary btn-sm" id="operating_system" type="button" disabled>OS</button>-->
<button class="btn btn-secondary btn-sm" id="cpu_load_button" type="button" disabled>
<!--<button class="btn btn-secondary btn-sm" id="cpu_load_button" type="button" disabled>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cpu" viewBox="0 0 16 16">
<path d="M5 0a.5.5 0 0 1 .5.5V2h1V.5a.5.5 0 0 1 1 0V2h1V.5a.5.5 0 0 1 1 0V2h1V.5a.5.5 0 0 1 1 0V2A2.5 2.5 0 0 1 14 4.5h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14a2.5 2.5 0 0 1-2.5 2.5v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14A2.5 2.5 0 0 1 2 11.5H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2A2.5 2.5 0 0 1 4.5 2V.5A.5.5 0 0 1 5 0zm-.5 3A1.5 1.5 0 0 0 3 4.5v7A1.5 1.5 0 0 0 4.5 13h7a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 11.5 3h-7zM5 6.5A1.5 1.5 0 0 1 6.5 5h3A1.5 1.5 0 0 1 11 6.5v3A1.5 1.5 0 0 1 9.5 11h-3A1.5 1.5 0 0 1 5 9.5v-3zM6.5 6a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3z" />
</svg> <span id="cpu_load">---</span>
@ -713,17 +778,17 @@
<path d="M1 3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4.586a1 1 0 0 0 .707-.293l.353-.353a.5.5 0 0 1 .708 0l.353.353a1 1 0 0 0 .707.293H15a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H1Zm.5 1h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-4a.5.5 0 0 1 .5-.5Zm5 0h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-4a.5.5 0 0 1 .5-.5Zm4.5.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-4ZM2 10v2H1v-2h1Zm2 0v2H3v-2h1Zm2 0v2H5v-2h1Zm3 0v2H8v-2h1Zm2 0v2h-1v-2h1Zm2 0v2h-1v-2h1Zm2 0v2h-1v-2h1Z" />
</svg> <span id="ram_load">---</span>
</button>
-->
</div>
</div>
</div>
</div>
</div>
<!--<hr class="m-1">-->
<div class="container mt-2 p-0">
<div class="row collapse multi-collapse" id="collapseThirdRow">
<div class="col-5">
<div class="card text-dark mb-1">
<div class="card-header p-1"><strong>AUDIO LEVEL</strong>
<div class="card-header p-1"><i class="bi bi-volume-up" style="font-size: 1rem; color: black;"></i> <strong>AUDIO LEVEL</strong>
</div>
<div class="card-body p-2">
<div class="progress mb-1" style="height: 20px;">
@ -740,26 +805,33 @@
<div class="card-body p-2">
<div class="row">
<div class="col-md-auto">
<div class="input-group input-group-sm mb-0" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="Send a ping request to a remote station"> <span class="input-group-text">Ping</span>
<input type="text" class="form-control" style="max-width: 6rem; text-transform:uppercase" placeholder="DXcall" pattern="[A-Z]*" id="dxCall" maxlength="6" aria-label="Input group" aria-describedby="btnGroupAddon">
<button class="btn btn-success" id="sendPing" type="button">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-arrows-angle-contract" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M.172 15.828a.5.5 0 0 0 .707 0l4.096-4.096V14.5a.5.5 0 1 0 1 0v-3.975a.5.5 0 0 0-.5-.5H1.5a.5.5 0 0 0 0 1h2.768L.172 15.121a.5.5 0 0 0 0 .707zM15.828.172a.5.5 0 0 0-.707 0l-4.096 4.096V1.5a.5.5 0 1 0-1 0v3.975a.5.5 0 0 0 .5.5H14.5a.5.5 0 0 0 0-1h-2.768L15.828.879a.5.5 0 0 0 0-.707z" />
</svg>
<div class="input-group input-group-sm mb-0">
<input type="text" class="form-control" style="max-width: 6rem; text-transform:uppercase" placeholder="DXcall" pattern="[A-Z]*" id="dxCall" maxlength="11" aria-label="Input group" aria-describedby="btnGroupAddon">
<button class="btn btn-sm btn-primary" id="sendPing" type="button" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="Send a ping request to a remote station">
Ping
</button>
<button class="btn btn-sm btn-success" id="openARQSession" type="button" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="connect to a remote station">
<i class="bi bi-arrows-angle-contract" style="font-size: 0.8rem; color: white;"></i>
</button>
<button class="btn btn-sm btn-danger" id="closeARQSession" type="button" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="disconnect from a remote station">
<i class="bi bi-arrows-angle-expand" style="font-size: 0.8rem; color: white;"></i>
</button>
</div>
</div>
<div class="col-md-auto">
<div class="input-group input-group-sm mb-0" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="Send a CQ to the world">
<button class="btn btn-success" id="sendCQ" type="button">CQ CQ CQ</button>
<button class="btn btn-sm btn-success" id="sendCQ" type="button">CQ CQ</button>
</div>
</div>
<div class="col-md-auto">
<div class="input-group input-group-sm" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="Start or stop the Beacon mode. You can also set the interval. While sending a beacon, you can receive ping requests and open a datachannel. If a datachannel is openend, the beacon pauses.">
<button type="button" id="startBeacon" class="btn btn-success">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-arrow-repeat" viewBox="0 0 16 16">
<path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z" />
<path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z" />
</svg>
<button type="button" id="startBeacon" class="btn btn-sm btn-success">
<i class="bi bi-arrow-clockwise" style="font-size: 0.8rem; color: white;"></i>
</button>
<div class="input-group-text p-1">Beacon</div>
<select class="form-select form-select-sm" aria-label=".form-select-sm" id="beaconInterval" style="width:5rem">
@ -769,11 +841,17 @@
<option value="30">30s</option>
<option value="45">45s</option>
<option value="60">60s</option>
<option value="90">90s</option>+</select>
<option value="90">90s</option>
<option value="120">2min</option>
<option value="300">5min</option>
<option value="600">10min</option>
<option value="900">15min</option>
<option value="1800">30min</option>
<option value="3600">60min</option>
</select>
<button type="button" id="stopBeacon" class="btn btn-sm btn-danger">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-x-octagon-fill" viewBox="0 0 16 16">
<path d="M11.46.146A.5.5 0 0 0 11.107 0H4.893a.5.5 0 0 0-.353.146L.146 4.54A.5.5 0 0 0 0 4.893v6.214a.5.5 0 0 0 .146.353l4.394 4.394a.5.5 0 0 0 .353.146h6.214a.5.5 0 0 0 .353-.146l4.394-4.394a.5.5 0 0 0 .146-.353V4.893a.5.5 0 0 0-.146-.353L11.46.146zm-6.106 4.5L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 1 1 .708-.708z" />
</svg>
<i class="bi bi-x-octagon-fill" style="font-size: 0.8rem; color: white;"></i>
</button>
</div>
</div>
@ -795,6 +873,7 @@
<label class="btn btn-sm btn-outline-secondary" for="waterfall-scatter-switch2"><strong>SCATTER</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">
<!--278px-->
@ -807,9 +886,8 @@
<div class="card text-dark mb-1" style="height: 240px">
<!--325px-->
<div class="card-header p-1">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-list-columns-reverse" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M0 .5A.5.5 0 0 1 .5 0h2a.5.5 0 0 1 0 1h-2A.5.5 0 0 1 0 .5Zm4 0a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1h-10A.5.5 0 0 1 4 .5Zm-4 2A.5.5 0 0 1 .5 2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5Zm4 0a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5Zm-4 2A.5.5 0 0 1 .5 4h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5Zm4 0a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5Zm-4 2A.5.5 0 0 1 .5 6h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5Zm4 0a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1h-8a.5.5 0 0 1-.5-.5Zm-4 2A.5.5 0 0 1 .5 8h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5Zm4 0a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1h-8a.5.5 0 0 1-.5-.5Zm-4 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5Zm4 0a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1h-10a.5.5 0 0 1-.5-.5Zm-4 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5Zm4 0a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5Zm-4 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5Zm4 0a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5Z" />
</svg><strong> HEARD STATIONS</strong>
<i class="bi bi-list-columns-reverse" style="font-size: 1rem; color: black;" style="font-size: 1rem; color: black;"></i>
<strong> HEARD STATIONS</strong>
</div>
<div class="card-body p-0">
<!-- START OF TABLE FOR HEARD STATIONS -->
@ -847,8 +925,11 @@
<!------------------------------- RECEIVED FILES SIDEBAR ----------------------->
<div class="offcanvas offcanvas-end" tabindex="-1" id="receivedFilesSidebar" aria-labelledby="receivedFilesSidebarLabel">
<div class="offcanvas-header p-2">
<button class="btn btn-sm btn-primary me-2" id="openReceivedFilesFolder" type="button">
<i class="bi bi-folder2-open" style="font-size: 1rem; color: white;"></i>
</button>
<h5 id="receivedFilesSidebarLabel">
Received Files
Received Files
</h5>
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
@ -898,7 +979,7 @@
<div class="row">
<div class="col-auto">
<div class="input-group input-group-sm mb-0">
<input type="text" class="form-control" style="max-width: 6rem; text-transform:uppercase" pattern="[A-Z]" placeholder="DXcall" id="dataModalDxCall" maxlength="6" aria-label="Input group" aria-describedby="btnGroupAddon">
<input type="text" class="form-control" style="max-width: 6rem; text-transform:uppercase" pattern="[A-Z]" placeholder="DXcall" id="dataModalDxCall" maxlength="11" aria-label="Input group" aria-describedby="btnGroupAddon">
</div>
</div>
<div class="col-auto">
@ -940,18 +1021,17 @@
<div class="row">
<div class="col">
<div class="input-group input-group-sm"> <span class="input-group-text" id="basic-addon1">Mode</span>
<select class="form-select form-select-sm" aria-label=".form-select-sm" id="datamode">
<!--<option value="14">low SNR (DC0)</option>-->
<select class="form-select form-select-sm" aria-label=".form-select-sm" id="datamode" disabled>
<option selected value="255">AUTO</option>
<option value="10">HIGH SNR (DC1)</option>
<option value="12">MED SNR (DC3)</option>
<option value="14">LOW SNR (DC0)</option>
<!--<option value="232">HIGH SNR (DC1)</option>-->
<!--<option value="231">MED SNR (DC3)</option>-->
<!--<option value="230">LOW SNR (DC0)</option>-->
</select>
</div>
</div>
<div class="col-auto">
<div class="input-group input-group-sm"> <span class="input-group-text" id="basic-addon1">Frames</span>
<select class="form-select form-select-sm" aria-label=".form-select-sm" id="framesperburst">
<select class="form-select form-select-sm" aria-label=".form-select-sm" id="framesperburst" disabled>
<option selected value="1">1</option>
</select>
</div>
@ -965,10 +1045,10 @@
</div>
<div class="row mb-1">
<div class="col">
<button type="button" id="startTransmission" data-bs-dismiss="offcanvas" class="btn btn-success" style="width:100%" disabled>START TRANSMISSION</button>
<button type="button" id="startTransmission" data-bs-dismiss="offcanvas" class="btn btn-success" style="width:100%">START TRANSMISSION</button>
</div>
<div class="col-md-auto">
<button type="button" id="stopTransmission" class="btn btn-danger" style="width:100%" disabled>STOP</button>
<button type="button" id="stopTransmission" class="btn btn-danger" style="width:100%">STOP</button>
</div>
</div>
<!--
@ -1000,24 +1080,30 @@
<div class="container-fluid">
<div class="btn-toolbar" role="toolbar">
<div class="btn-group btn-group-sm me-2" role="group">
<button class="btn 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>">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-broadcast-pin" viewBox="0 0 16 16">
<path d="M3.05 3.05a7 7 0 0 0 0 9.9.5.5 0 0 1-.707.707 8 8 0 0 1 0-11.314.5.5 0 0 1 .707.707zm2.122 2.122a4 4 0 0 0 0 5.656.5.5 0 1 1-.708.708 5 5 0 0 1 0-7.072.5.5 0 0 1 .708.708zm5.656-.708a.5.5 0 0 1 .708 0 5 5 0 0 1 0 7.072.5.5 0 1 1-.708-.708 4 4 0 0 0 0-5.656.5.5 0 0 1 0-.708zm2.122-2.12a.5.5 0 0 1 .707 0 8 8 0 0 1 0 11.313.5.5 0 0 1-.707-.707 7 7 0 0 0 0-9.9.5.5 0 0 1 0-.707zM6 8a2 2 0 1 1 2.5 1.937V15.5a.5.5 0 0 1-1 0V9.937A2 2 0 0 1 6 8z" />
</svg>
<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">
<button class="btn 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>">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cpu" viewBox="0 0 16 16">
<path d="M5 0a.5.5 0 0 1 .5.5V2h1V.5a.5.5 0 0 1 1 0V2h1V.5a.5.5 0 0 1 1 0V2h1V.5a.5.5 0 0 1 1 0V2A2.5 2.5 0 0 1 14 4.5h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14v1h1.5a.5.5 0 0 1 0 1H14a2.5 2.5 0 0 1-2.5 2.5v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14h-1v1.5a.5.5 0 0 1-1 0V14A2.5 2.5 0 0 1 2 11.5H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2v-1H.5a.5.5 0 0 1 0-1H2A2.5 2.5 0 0 1 4.5 2V.5A.5.5 0 0 1 5 0zm-.5 3A1.5 1.5 0 0 0 3 4.5v7A1.5 1.5 0 0 0 4.5 13h7a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 11.5 3h-7zM5 6.5A1.5 1.5 0 0 1 6.5 5h3A1.5 1.5 0 0 1 11 6.5v3A1.5 1.5 0 0 1 9.5 11h-3A1.5 1.5 0 0 1 5 9.5v-3zM6.5 6a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3z" />
</svg>
<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">
<button class="btn 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>">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-left-right" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1 11.5a.5.5 0 0 0 .5.5h11.793l-3.147 3.146a.5.5 0 0 0 .708.708l4-4a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 11H1.5a.5.5 0 0 0-.5.5zm14-7a.5.5 0 0 1-.5.5H2.707l3.147 3.146a.5.5 0 1 1-.708.708l-4-4a.5.5 0 0 1 0-.708l4-4a.5.5 0 1 1 .708.708L2.707 4H14.5a.5.5 0 0 1 .5.5z" />
</svg>
<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">
<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>
@ -1029,13 +1115,18 @@
</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"><strong>B/min</strong></span>
<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">
<i id="speed_level" class="bi bi-reception-0" style="font-size: 1rem; color: black;"></i></span>
<span class="input-group-text" id="bytes_per_min" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="raw data rate modem in bytes per minute">---</span>
<span class="input-group-text" id="bytes_per_min_compressed" data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-html="false" title="data rate including file compression in bytes per minute">---</span>
</div>
</div>
<div class="container-fluid p-0" style="width:10rem">
<div class="input-group input-group-sm"> <span class="input-group-text" id="basic-addon1"><strong>kBytes</strong></span>
<div class="input-group input-group-sm"> <span class="input-group-text" id="basic-addon1"><strong>Bytes</strong></span>
<span class="input-group-text" id="total_bytes">---</span>
</div>
</div>
@ -1058,6 +1149,67 @@
<script src="waterfall/spectrum.js"></script>
<script src="waterfall/spectrogram.js"></script>
<!--<script src="waterfall/script.js"></script>-->
<!-- INFO MODAL -->
<div class="modal fade" data-bs-backdrop="static" tabindex="-1" id="infoModal">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">App settings</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="input-group input-group-sm mb-1"> <span class="input-group-text" id="basic-addon1">Theme</span>
<select class="form-select form-select-sm" id="theme_selector">
<option value="default">Default</option>
<option value="cerulean">Cerulean</option>
<option value="cosmo">Cosmo</option>
<option value="cyborg">Cyborg</option>
<option value="darkly">Darkly</option>
<option value="flatly">Flatly</option>
<option value="journal">Journal</option>
<option value="litera">Litera</option>
<option value="lumen">Lumen</option>
<option value="lux">Lux</option>
<option value="materia">Materia</option>
<option value="minty">Minty</option>
<option value="morph">Morhp</option>
<option value="pulse">Pulse</option>
<option value="quartz">Quartz</option>
<option value="sandstone">Sandstone</option>
<option value="simplex">Simplex</option>
<option value="sketchy">Sketchy</option>
<option value="slate">Slate</option>
<option value="solar">Solar</option>
<option value="spacelab">Spacelab</option>
<option value="superhero">Superhero</option>
<option value="united">United</option>
<option value="vapor">Vapor</option>
<option value="yeti">Yeti</option>
<option value="zephyr">Zephyr</option>
</select>
</div>
<div class="input-group input-group-sm mb-1"> <span class="input-group-text" id="basic-addon1">Update channel</span>
<select class="form-select form-select-sm" id="update_channel_selector">
<option value="latest">stable</option>
<option value="beta">beta</option>
<option value="alpha">alpha</option>
</select>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text" for="inputGroupFile02">Received files folder</label>
<input type="text" class="form-control" id="received_files_folder">
</div>
</div>
</div>
</div>
</div>
<!-- HAMLIB ADVANCED SETTINGS MODAL -->
<div class="modal fade" data-bs-backdrop="static" tabindex="-1" id="advancedHamlibSettingsModal">
<div class="modal-dialog">
@ -1069,7 +1221,7 @@
<div class="modal-body">
<div class="input-group input-group-sm mb-1"> <span class="input-group-text" id="basic-addon1">Port</span>
<select class="form-select form-select-sm" aria-label=".form-select-sm" id="hamlib_deviceport_advanced">
<!--<option selected value="/dev/ttyUSB0">/dev/ttyUSB0</option>
<!--<option value="None">None</option>-->
<option value="/dev/ttyUSB1">/dev/ttyUSB1</option>-->
</select> <span class="input-group-text" id="basic-addon1">Speed</span>
<select class="form-select form-select-sm" aria-label=".form-select-sm" id="hamlib_serialspeed_advanced">
@ -1087,11 +1239,12 @@
</div>
<div class="input-group input-group-sm mb-1"> <span class="input-group-text" id="basic-addon1">PTT</span>
<select class="form-select form-select-sm" aria-label=".form-select-sm" id="hamlib_ptt_protocol_advanced" style="width: 0.5rem">
<option value="NONE">NONE</option>
<option value="RIG">RIG</option>
<option value="USB">USB</option>
<option value="RTS">Serial RTS</option>
<option value="DTR-H">Serial DTR-High</option>
<option value="DTR-H">Serial DTR-Low</option>
<option value="DTR-L">Serial DTR-Low</option>
<option value="PARALLEL">Rig PARALLEL</option>
<option value="MICDATA">Rig MICDATA</option>
<option value="CM108">Rig CM108</option>
@ -1116,7 +1269,9 @@
</div>
<div class="input-group input-group-sm mb-1"> <span class="input-group-text" id="basic-addon1">PTT Port</span>
<select class="form-select form-select-sm" aria-label=".form-select-sm" id="hamlib_ptt_port_advanced">
<!--<option selected value="/dev/ttyUSB0">/dev/ttyUSB0</option>
<option value="None">None</option>
</select>
<!--
<option value="/dev/ttyUSB1">/dev/ttyUSB1</option>-->
<!--
</select> <span class="input-group-text" id="basic-addon1">PTT Speed</span>
@ -1144,4 +1299,4 @@
</div>
</body>
</html>
</html>

40
gui/src/log-module.html Normal file
View file

@ -0,0 +1,40 @@
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<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">
<title>FreeDATA - Live Log</title>
</head>
<body>
<!-- bootstrap -->
<script src="../node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<!-- chart.js -->
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Timestamp</th>
<th scope="col">Log entry</th>
</tr>
</thead>
<tbody id="log">
<!--
<tr>
<th scope="row">1</th>
<td>Mark</td>
<td>Otto</td>
<td>@mdo</td>
</tr>
-->
</tbody>
</table>
</body>
</html>

1
test/in8.raw Normal file

File diff suppressed because one or more lines are too long

BIN
test/out48.raw Normal file

Binary file not shown.

BIN
test/out8.raw Normal file

Binary file not shown.

107
tnc/audio.py Normal file
View file

@ -0,0 +1,107 @@
import json
import sys
import multiprocessing
####################################################
# https://stackoverflow.com/questions/7088672/pyaudio-working-but-spits-out-error-messages-each-time
# https://github.com/DJ2LS/FreeDATA/issues/22
# we need to have a look at this if we want to run this on Windows and MacOS !
# Currently it seems, this is a Linux-only problem
from ctypes import *
from contextlib import contextmanager
import pyaudio
ERROR_HANDLER_FUNC = CFUNCTYPE(None, c_char_p, c_int, c_char_p, c_int, c_char_p)
def py_error_handler(filename, line, function, err, fmt):
"""
Args:
filename:
line:
function:
err:
fmt:
Returns:
"""
pass
c_error_handler = ERROR_HANDLER_FUNC(py_error_handler)
@contextmanager
def noalsaerr():
""" """
asound = cdll.LoadLibrary('libasound.so')
asound.snd_lib_error_set_handler(c_error_handler)
yield
asound.snd_lib_error_set_handler(None)
# with noalsaerr():
# p = pyaudio.PyAudio()
#####################################################
def get_audio_devices():
"""
return list of input and output audio devices in own process to avoid crashes of portaudio on raspberry pi
also uses a process data manager
"""
# we need to run this on windows for multiprocessing support
# multiprocessing.freeze_support()
#multiprocessing.get_context('spawn')
with multiprocessing.Manager() as manager:
proxy_input_devices = manager.list()
proxy_output_devices = manager.list()
#print(multiprocessing.get_start_method())
p = multiprocessing.Process(target=fetch_audio_devices, args=(proxy_input_devices, proxy_output_devices))
p.start()
p.join()
return list(proxy_input_devices), list(proxy_output_devices)
def fetch_audio_devices(input_devices, output_devices):
"""
get audio devices from portaudio
Args:
input_devices: proxy variable for input devices
output_devices: proxy variable for outout devices
Returns:
"""
# UPDATE LIST OF AUDIO DEVICES
try:
# we need to "try" this, because sometimes libasound.so isn't in the default place
# try to supress error messages
with noalsaerr(): # https://github.com/DJ2LS/FreeDATA/issues/22
p = pyaudio.PyAudio()
# else do it the default way
except Exception as e:
p = pyaudio.PyAudio()
#input_devices = []
#output_devices = []
for i in range(0, p.get_device_count()):
# we need to do a try exception, beacuse for windows theres no audio device range
try:
maxInputChannels = p.get_device_info_by_host_api_device_index(0, i).get('maxInputChannels')
maxOutputChannels = p.get_device_info_by_host_api_device_index(0, i).get('maxOutputChannels')
name = p.get_device_info_by_host_api_device_index(0, i).get('name')
except:
maxInputChannels = 0
maxOutputChannels = 0
name = ''
if maxInputChannels > 0:
input_devices.append({"id": i, "name": str(name)})
if maxOutputChannels > 0:
output_devices.append({"id": i, "name": str(name)})
p.terminate()

View file

@ -5,14 +5,18 @@ import ctypes
from ctypes import *
import sys
import os
#import pathlib
from enum import Enum
import numpy as np
#print("loading codec2 module", file=sys.stderr)
from threading import Lock
import glob
import structlog
# Enum for codec2 modes
class FREEDV_MODE(Enum):
"""
enum for codec2 modes and names
"""
datac0 = 14
datac1 = 10
datac3 = 12
@ -20,10 +24,26 @@ class FREEDV_MODE(Enum):
# function for returning the mode value
def freedv_get_mode_value_by_name(mode):
"""
get the codec2 mode by entering its string
Args:
mode:
Returns: int
"""
return FREEDV_MODE[mode].value
# function for returning the mode name
def freedv_get_mode_name_by_value(mode):
"""
get the codec2 mode name as string
Args:
mode:
Returns: string
"""
return FREEDV_MODE(mode).name
@ -34,48 +54,41 @@ except:
app_path = os.path.abspath(".")
sys.path.append(app_path)
# -------------------------------------------- LOAD FREEDV
# codec2 search pathes in descending order
# libcodec2.so ctests
# pathlib.Path("codec2/build_linux/src/libcodec2.so.1.0") manual build
# pathlib.Path("lib/codec2/linux/libcodec2.so.1.0") precompiled
# pathlib.Path("../../tnc/codec2/build_linux/src/libcodec2.so.1.0") external loading manual build
# pathlib.Path("../../tnc/lib/codec2/linux/libcodec2.so.1.0") external loading precompiled
structlog.get_logger("structlog").info("[C2 ] Searching for libcodec2...")
if sys.platform == 'linux':
libname = ["libcodec2.so", \
os.path.join(app_path, "codec2/build_linux/src/libcodec2.so.1.0"), \
os.path.join(app_path, "lib/codec2/linux/libcodec2.so.1.0"), \
os.path.join(app_path, "../tnc/codec2/build_linux/src/libcodec2.so.1.0"), \
os.path.join(app_path, "../tnc/lib/codec2/linux/libcodec2.so.1.0"), \
]
elif sys.platform == 'win32' or sys.platform == 'win64':
libname = [app_path + "\\lib\\codec2\\windows\\libcodec2.dll", \
]
else:
print(f"[C2 ] Platform not supported {sys.platform}", file=sys.stderr)
files = glob.glob('**/*libcodec2*',recursive=True)
files.append('libcodec2.so')
elif sys.platform == 'darwin':
files = glob.glob('**/*libcodec2*.dylib',recursive=True)
# iterate through codec2 search pathes
for i in libname:
elif sys.platform == 'win32' or sys.platform == 'win64':
files = glob.glob('**\*libcodec2*.dll',recursive=True)
else:
files = []
for file in files:
try:
# this is not working for all OS. Specially windows has some more problems. We need to fix this somehow.
api = ctypes.CDLL(i)
print(f"[C2 ] Codec2 library found - {i}", file=sys.stderr)
api = ctypes.CDLL(file)
structlog.get_logger("structlog").info("[C2 ] Libcodec2 loaded", path=file)
break
except:
print(f"[C2 ] Codec2 library not found - {i}", file=sys.stderr)
pass
except Exception as e:
structlog.get_logger("structlog").warning("[C2 ] Libcodec2 found but not loaded", path=file, e=e)
# quit module if codec2 cant be loaded
if not 'api' in locals():
print(f"[C2 ] Loading Codec2 library failed", file=sys.stderr)
structlog.get_logger("structlog").critical("[C2 ] Libcodec2 not loaded", path=file)
os._exit(1)
# ctypes function init
#api.freedv_set_tuning_range.restype = c_int
#api.freedv_set_tuning_range.argype = [c_void_p, c_float, c_float]
api.freedv_open.argype = [c_int]
api.freedv_open.restype = c_void_p
@ -153,15 +166,29 @@ api.rx_sync_flags_to_text = [
# audio buffer ---------------------------------------------------------
class audio_buffer:
"""
thread safe audio buffer, which fits to needs of codec2
made by David Rowe, VK5DGR
"""
# a buffer of int16 samples, using a fixed length numpy array self.buffer for storage
# self.nbuffer is the current number of samples in the buffer
def __init__(self, size):
print("create audio_buffer: ", size)
structlog.get_logger("structlog").debug("[C2 ] creating audio buffer", size=size)
self.size = size
self.buffer = np.zeros(size, dtype=np.int16)
self.nbuffer = 0
self.mutex = Lock()
def push(self,samples):
"""
Push new data to buffer
Args:
samples:
Returns:
"""
self.mutex.acquire()
# add samples at the end of the buffer
assert self.nbuffer+len(samples) <= self.size
@ -169,6 +196,14 @@ class audio_buffer:
self.nbuffer += len(samples)
self.mutex.release()
def pop(self,size):
"""
get data from buffer in size of NIN
Args:
size:
Returns:
"""
self.mutex.acquire()
# remove samples from the start of the buffer
self.nbuffer -= size;
@ -185,17 +220,29 @@ api.fdmdv_8_to_48_short.argtype = [c_void_p, c_void_p, c_int]
api.fdmdv_48_to_8_short.argtype = [c_void_p, c_void_p, c_int]
class resampler:
"""
resampler class
"""
# resample an array of variable length, we just store the filter memories here
MEM8 = api.FDMDV_OS_TAPS_48_8K
MEM48 = api.FDMDV_OS_TAPS_48K
def __init__(self):
print("create 48<->8 kHz resampler")
structlog.get_logger("structlog").debug("[C2 ] create 48<->8 kHz resampler")
self.filter_mem8 = np.zeros(self.MEM8, dtype=np.int16)
self.filter_mem48 = np.zeros(self.MEM48)
def resample48_to_8(self,in48):
"""
audio resampler integration from codec2
downsample audio from 48000Hz to 8000Hz
Args:
in48: input data as np.int16
Returns: downsampled 8000Hz data as np.int16
"""
assert in48.dtype == np.int16
# length of input vector must be an integer multiple of api.FDMDV_OS_48
assert(len(in48) % api.FDMDV_OS_48 == 0)
@ -217,6 +264,15 @@ class resampler:
return out8
def resample8_to_48(self,in8):
"""
audio resampler integration from codec2
resample audio from 8000Hz to 48000Hz
Args:
in8: input data as np.int16
Returns: 48000Hz audio as np.int16
"""
assert in8.dtype == np.int16
# concat filter memory and input samples

View file

@ -5,6 +5,8 @@ daemon.py
Author: DJ2LS, January 2022
daemon for providing basic information for the tnc like audio or serial devices
"""
import argparse
@ -23,217 +25,202 @@ import structlog
import log_handler
import helpers
import os
import queue
import audio
import sock
import atexit
import signal
import multiprocessing
log_handler.setup_logging("daemon")
structlog.get_logger("structlog").info("[DMN] Starting FreeDATA daemon", author="DJ2LS", year="2022", version="0.1")
# signal handler for closing aplication
def signal_handler(sig, frame):
"""
signal handler for closing the network socket on app exit
Args:
sig:
frame:
# get python version, which is needed later for determining installation path
python_version = str(sys.version_info[0]) + "." + str(sys.version_info[1])
structlog.get_logger("structlog").info("[DMN] Python", version=python_version)
Returns: system exit
"""
print('Closing daemon...')
sock.CLOSE_SIGNAL = True
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
####################################################
# https://stackoverflow.com/questions/7088672/pyaudio-working-but-spits-out-error-messages-each-time
# https://github.com/DJ2LS/FreeDATA/issues/22
# we need to have a look at this if we want to run this on Windows and MacOS !
# Currently it seems, this is a Linux-only problem
from ctypes import *
from contextlib import contextmanager
import pyaudio
ERROR_HANDLER_FUNC = CFUNCTYPE(None, c_char_p, c_int, c_char_p, c_int, c_char_p)
def py_error_handler(filename, line, function, err, fmt):
pass
c_error_handler = ERROR_HANDLER_FUNC(py_error_handler)
@contextmanager
def noalsaerr():
asound = cdll.LoadLibrary('libasound.so')
asound.snd_lib_error_set_handler(c_error_handler)
yield
asound.snd_lib_error_set_handler(None)
class DAEMON():
"""
daemon class
# with noalsaerr():
# p = pyaudio.PyAudio()
######################################################
"""
def __init__(self):
# load crc engine
self.crc_algorithm = crcengine.new('crc16-ccitt-false') # load crc8 library
self.daemon_queue = sock.DAEMON_QUEUE
update_audio_devices = threading.Thread(target=self.update_audio_devices, name="UPDATE_AUDIO_DEVICES", daemon=True)
update_audio_devices.start()
update_serial_devices = threading.Thread(target=self.update_serial_devices, name="UPDATE_SERIAL_DEVICES", daemon=True)
update_serial_devices.start()
# load crc engine
crc_algorithm = crcengine.new('crc16-ccitt-false') # load crc8 library
def start_daemon():
try:
structlog.get_logger("structlog").info("[DMN] Starting TCP/IP socket", port=PORT)
# https://stackoverflow.com/a/16641793
socketserver.TCPServer.allow_reuse_address = True
daemon = socketserver.TCPServer(('0.0.0.0', PORT), CMDTCPRequestHandler)
daemon.serve_forever()
finally:
structlog.get_logger("structlog").warning("[DMN] Closing socket", port=PORT)
daemon.server_close()
class CMDTCPRequestHandler(socketserver.BaseRequestHandler):
def handle(self, hamlib_version = 0):
structlog.get_logger("structlog").debug("[DMN] Client connected", ip=self.client_address[0])
# loop through socket buffer until timeout is reached. then close buffer
socketTimeout = time.time() + 6
while socketTimeout > time.time():
time.sleep(0.01)
encoding = 'utf-8'
#data = str(self.request.recv(1024), 'utf-8')
data = bytes()
# we need to loop through buffer until end of chunk is reached or timeout occured
while socketTimeout > time.time():
data += self.request.recv(64)
# or chunk.endswith(b'\n'):
if data.startswith(b'{"type"') and data.endswith(b'}\n'):
break
data = data[:-1] # remove b'\n'
data = str(data, encoding)
if len(data) > 0:
# reset socket timeout
socketTimeout = time.time() + static.SOCKET_TIMEOUT
# only read first line of string. multiple lines will cause an json error
# this occurs possibly, if we are getting data too fast
# data = data.splitlines()[0]
data = data.splitlines()[0]
# we need to do some error handling in case of socket timeout or decoding issue
worker = threading.Thread(target=self.worker, name="WORKER", daemon=True)
worker.start()
def update_audio_devices(self):
"""
update audio devices and set to static
"""
while 1:
try:
# convert data to json object
received_json = json.loads(data)
# GET COMMANDS
# "command" : "..."
# SET COMMANDS
# "command" : "..."
# "parameter" : " ..."
# DATA COMMANDS
# "command" : "..."
# "type" : "..."
# "dxcallsign" : "..."
# "data" : "..."
# print(received_json)
# print(received_json["type"])
# print(received_json["command"])
# try:
if received_json["type"] == 'SET' and received_json["command"] == 'MYCALLSIGN':
callsign = received_json["parameter"]
print(received_json)
if bytes(callsign, 'utf-8') == b'':
self.request.sendall(b'INVALID CALLSIGN')
structlog.get_logger("structlog").warning("[DMN] SET MYCALL FAILED", call=static.MYCALLSIGN, crc=static.MYCALLSIGN_CRC8)
else:
static.MYCALLSIGN = bytes(callsign, 'utf-8')
static.MYCALLSIGN_CRC8 = helpers.get_crc_8(static.MYCALLSIGN)
structlog.get_logger("structlog").info("[DMN] SET MYCALL", call=static.MYCALLSIGN, crc=static.MYCALLSIGN_CRC8)
if received_json["type"] == 'SET' and received_json["command"] == 'MYGRID':
mygrid = received_json["parameter"]
if bytes(mygrid, 'utf-8') == b'':
self.request.sendall(b'INVALID GRID')
else:
static.MYGRID = bytes(mygrid, 'utf-8')
structlog.get_logger("structlog").info("[DMN] SET MYGRID", grid=static.MYGRID)
if received_json["type"] == 'SET' and received_json["command"] == 'STARTTNC' and not static.TNCSTARTED:
mycall = str(received_json["parameter"][0]["mycall"])
mygrid = str(received_json["parameter"][0]["mygrid"])
rx_audio = str(received_json["parameter"][0]["rx_audio"])
tx_audio = str(received_json["parameter"][0]["tx_audio"])
devicename = str(received_json["parameter"][0]["devicename"])
deviceport = str(received_json["parameter"][0]["deviceport"])
serialspeed = str(received_json["parameter"][0]["serialspeed"])
pttprotocol = str(received_json["parameter"][0]["pttprotocol"])
pttport = str(received_json["parameter"][0]["pttport"])
data_bits = str(received_json["parameter"][0]["data_bits"])
stop_bits = str(received_json["parameter"][0]["stop_bits"])
handshake = str(received_json["parameter"][0]["handshake"])
radiocontrol = str(received_json["parameter"][0]["radiocontrol"])
rigctld_ip = str(received_json["parameter"][0]["rigctld_ip"])
rigctld_port = str(received_json["parameter"][0]["rigctld_port"])
if not static.TNCSTARTED:
structlog.get_logger("structlog").warning("[DMN] Starting TNC", rig=devicename, port=deviceport)
#print(received_json["parameter"][0])
static.AUDIO_INPUT_DEVICES, static.AUDIO_OUTPUT_DEVICES = audio.get_audio_devices()
except Exception as e:
print(e)
time.sleep(1)
def update_serial_devices(self):
"""
update serial devices and set to static
"""
while 1:
try:
#print("update serial")
serial_devices = []
ports = serial.tools.list_ports.comports()
for port, desc, hwid in ports:
# calculate hex of hwid if we have unique names
crc_hwid = self.crc_algorithm(bytes(hwid, encoding='utf-8'))
crc_hwid = crc_hwid.to_bytes(2, byteorder='big')
crc_hwid = crc_hwid.hex()
description = desc + ' [' + crc_hwid + ']'
serial_devices.append({"port": str(port), "description": str(description) })
static.SERIAL_DEVICES = serial_devices
time.sleep(1)
except Exception as e:
print(e)
def worker(self):
"""
a worker for the received commands
"""
while 1:
try:
data = self.daemon_queue.get()
# command = "--rx "+ rx_audio +" \
# --tx "+ tx_audio +" \
# --deviceport "+ deviceport +" \
# --deviceid "+ deviceid + " \
# --serialspeed "+ serialspeed + " \
# --pttprotocol "+ pttprotocol + " \
# --pttport "+ pttport
# data[1] mycall
# data[2] mygrid
# data[3] rx_audio
# data[4] tx_audio
# data[5] devicename
# data[6] deviceport
# data[7] serialspeed
# data[8] pttprotocol
# data[9] pttport
# data[10] data_bits
# data[11] stop_bits
# data[12] handshake
# data[13] radiocontrol
# data[14] rigctld_ip
# data[15] rigctld_port
# data[16] send_scatter
# data[17] send_fft
# data[18] low_bandwith_mode
if data[0] == 'STARTTNC':
structlog.get_logger("structlog").warning("[DMN] Starting TNC", rig=data[5], port=data[6])
# list of parameters, necessary for running subprocess command as a list
options = []
options.append('--port')
options.append(str(static.DAEMONPORT - 1))
options.append('--mycall')
options.append(mycall)
options.append(data[1])
options.append('--mygrid')
options.append(mygrid)
options.append(data[2])
options.append('--rx')
options.append(rx_audio)
options.append(data[3])
options.append('--tx')
options.append(tx_audio)
options.append('--deviceport')
options.append(deviceport)
options.append('--devicename')
options.append(devicename)
options.append('--serialspeed')
options.append(serialspeed)
options.append('--pttprotocol')
options.append(pttprotocol)
options.append('--pttport')
options.append(pttport)
options.append('--data_bits')
options.append(data_bits)
options.append('--stop_bits')
options.append(stop_bits)
options.append('--handshake')
options.append(handshake)
options.append('--radiocontrol')
options.append(radiocontrol)
options.append('--rigctld_ip')
options.append(rigctld_ip)
options.append('--rigctld_port')
options.append(rigctld_port)
options.append(data[4])
# if radiocontrol != disabled
# this should hopefully avoid a ton of problems if we are just running in
# disabled mode
if data[13] != 'disabled':
options.append('--devicename')
options.append(data[5])
options.append('--deviceport')
options.append(data[6])
options.append('--serialspeed')
options.append(data[7])
options.append('--pttprotocol')
options.append(data[8])
options.append('--pttport')
options.append(data[9])
options.append('--data_bits')
options.append(data[10])
options.append('--stop_bits')
options.append(data[11])
options.append('--handshake')
options.append(data[12])
options.append('--radiocontrol')
options.append(data[13])
if data[13] != 'rigctld':
options.append('--rigctld_ip')
options.append(data[14])
options.append('--rigctld_port')
options.append(data[15])
if data[16] == 'True':
options.append('--scatter')
if data[17] == 'True':
options.append('--fft')
if data[18] == 'True':
options.append('--500hz')
# try running tnc from binary, else run from source
# this helps running the tnc in a developer environment
try:
command = []
if sys.platform == 'linux' or sys.platform == 'darwin':
command.append('./tnc')
command.append('./freedata-tnc')
elif sys.platform == 'win32' or sys.platform == 'win64':
command.append('tnc.exe')
command.append('freedata-tnc.exe')
command += options
p = subprocess.Popen(command)
atexit.register(p.kill)
structlog.get_logger("structlog").info("[DMN] TNC started", path="binary")
except:
command = []
@ -245,154 +232,129 @@ class CMDTCPRequestHandler(socketserver.BaseRequestHandler):
command.append('main.py')
command += options
p = subprocess.Popen(command)
atexit.register(p.kill)
structlog.get_logger("structlog").info("[DMN] TNC started", path="source")
static.TNCPROCESS = p # .pid
static.TNCSTARTED = True
'''
# WE HAVE THIS PART in SOCKET
if data[0] == 'STOPTNC':
static.TNCPROCESS.kill()
structlog.get_logger("structlog").warning("[DMN] Stopping TNC")
#os.kill(static.TNCPROCESS, signal.SIGKILL)
static.TNCSTARTED = False
'''
# data[1] devicename
# data[2] deviceport
# data[3] serialspeed
# data[4] pttprotocol
# data[5] pttport
# data[6] data_bits
# data[7] stop_bits
# data[8] handshake
# data[9] radiocontrol
# data[10] rigctld_ip
# data[11] rigctld_port
if data[0] == 'TEST_HAMLIB':
if received_json["type"] == 'SET' and received_json["command"] == 'STOPTNC':
static.TNCPROCESS.kill()
structlog.get_logger("structlog").warning("[DMN] Stopping TNC")
#os.kill(static.TNCPROCESS, signal.SIGKILL)
static.TNCSTARTED = False
devicename = data[1]
deviceport = data[2]
serialspeed = data[3]
pttprotocol = data[4]
pttport = data[5]
data_bits = data[6]
stop_bits = data[7]
handshake = data[8]
radiocontrol = data[9]
rigctld_ip = data[10]
rigctld_port = data[11]
if received_json["type"] == 'GET' and received_json["command"] == 'DAEMON_STATE':
data = {
'COMMAND': 'DAEMON_STATE',
'DAEMON_STATE': [],
'PYTHON_VERSION': str(python_version),
'HAMLIB_VERSION': str(hamlib_version),
'INPUT_DEVICES': [],
'OUTPUT_DEVICES': [],
'SERIAL_DEVICES': [
], "CPU": str(psutil.cpu_percent()), "RAM": str(psutil.virtual_memory().percent), "VERSION": "0.1-prototype"}
if static.TNCSTARTED:
data["DAEMON_STATE"].append({"STATUS": "running"})
# check how we want to control the radio
if radiocontrol == 'direct':
import rig
elif radiocontrol == 'rigctl':
import rigctl as rig
elif radiocontrol == 'rigctld':
import rigctld as rig
else:
data["DAEMON_STATE"].append({"STATUS": "stopped"})
# UPDATE LIST OF AUDIO DEVICES
try:
# we need to "try" this, because sometimes libasound.so isn't in the default place
# try to supress error messages
with noalsaerr(): # https://github.com/DJ2LS/FreeDATA/issues/22
p = pyaudio.PyAudio()
# else do it the default way
except Exception as e:
p = pyaudio.PyAudio()
for i in range(0, p.get_device_count()):
# we need to do a try exception, beacuse for windows theres now audio device range
try:
maxInputChannels = p.get_device_info_by_host_api_device_index(0, i).get('maxInputChannels')
maxOutputChannels = p.get_device_info_by_host_api_device_index(0, i).get('maxOutputChannels')
name = p.get_device_info_by_host_api_device_index(0, i).get('name')
except:
maxInputChannels = 0
maxOutputChannels = 0
name = ''
#crc_name = crc_algorithm(bytes(name, encoding='utf-8'))
#crc_name = crc_name.to_bytes(2, byteorder='big')
#crc_name = crc_name.hex()
#name = name + ' [' + crc_name + ']'
if maxInputChannels > 0:
data["INPUT_DEVICES"].append(
{"ID": i, "NAME": str(name)})
if maxOutputChannels > 0:
data["OUTPUT_DEVICES"].append(
{"ID": i, "NAME": str(name)})
p.terminate()
# UPDATE LIST OF SERIAL DEVICES
ports = serial.tools.list_ports.comports()
for port, desc, hwid in ports:
# calculate hex of hwid if we have unique names
crc_hwid = crc_algorithm(bytes(hwid, encoding='utf-8'))
crc_hwid = crc_hwid.to_bytes(2, byteorder='big')
crc_hwid = crc_hwid.hex()
description = desc + ' [' + crc_hwid + ']'
import rigdummy as rig
data["SERIAL_DEVICES"].append(
{"PORT": str(port), "DESCRIPTION": str(description) })
hamlib = rig.radio()
hamlib.open_rig(devicename=devicename, deviceport=deviceport, hamlib_ptt_type=pttprotocol, serialspeed=serialspeed, pttport=pttport, data_bits=data_bits, stop_bits=stop_bits, handshake=handshake, rigctld_ip=rigctld_ip, rigctld_port = rigctld_port)
hamlib_version = rig.hamlib_version
hamlib.set_ptt(True)
pttstate = hamlib.get_ptt()
jsondata = json.dumps(data)
self.request.sendall(bytes(jsondata, encoding))
if received_json["type"] == 'GET' and received_json["command"] == 'TEST_HAMLIB':
try:
print(received_json["parameter"])
devicename = str(received_json["parameter"][0]["devicename"])
deviceport = str(received_json["parameter"][0]["deviceport"])
serialspeed = str(received_json["parameter"][0]["serialspeed"])
pttprotocol = str(received_json["parameter"][0]["pttprotocol"])
pttport = str(received_json["parameter"][0]["pttport"])
data_bits = str(received_json["parameter"][0]["data_bits"])
stop_bits = str(received_json["parameter"][0]["stop_bits"])
handshake = str(received_json["parameter"][0]["handshake"])
radiocontrol = str(received_json["parameter"][0]["radiocontrol"])
rigctld_ip = str(received_json["parameter"][0]["rigctld_ip"])
rigctld_port = str(received_json["parameter"][0]["rigctld_port"])
if pttstate:
structlog.get_logger("structlog").info("[DMN] Hamlib PTT", status = 'SUCCESS')
response = {'command': 'test_hamlib', 'result': 'SUCCESS'}
elif not pttstate:
structlog.get_logger("structlog").warning("[DMN] Hamlib PTT", status = 'NO SUCCESS')
response = {'command': 'test_hamlib', 'result': 'NOSUCCESS'}
else:
structlog.get_logger("structlog").error("[DMN] Hamlib PTT", status = 'FAILED')
response = {'command': 'test_hamlib', 'result': 'FAILED'}
# check how we want to control the radio
if radiocontrol == 'direct':
import rig
elif radiocontrol == 'rigctl':
import rigctl as rig
elif radiocontrol == 'rigctld':
import rigctld as rig
else:
raise NotImplementedError
hamlib = rig.radio()
hamlib.open_rig(devicename=devicename, deviceport=deviceport, hamlib_ptt_type=pttprotocol, serialspeed=serialspeed, pttport=pttport, data_bits=data_bits, stop_bits=stop_bits, handshake=handshake, rigctld_ip=rigctld_ip, rigctld_port = rigctld_port)
hamlib_version = rig.hamlib_version
hamlib.set_ptt(False)
hamlib.close_rig()
hamlib.set_ptt(True)
pttstate = hamlib.get_ptt()
if pttstate:
structlog.get_logger("structlog").info("[DMN] Hamlib PTT", status = 'SUCCESS')
data = {'COMMAND': 'TEST_HAMLIB', 'RESULT': 'SUCCESS'}
elif not pttstate:
structlog.get_logger("structlog").warning("[DMN] Hamlib PTT", status = 'NO SUCCESS')
data = {'COMMAND': 'TEST_HAMLIB', 'RESULT': 'NOSUCCESS'}
else:
structlog.get_logger("structlog").error("[DMN] Hamlib PTT", status = 'FAILED')
data = {'COMMAND': 'TEST_HAMLIB', 'RESULT': 'FAILED'}
hamlib.set_ptt(False)
hamlib.close_rig()
jsondata = json.dumps(data)
self.request.sendall(bytes(jsondata, encoding))
except Exception as e:
structlog.get_logger("structlog").error("[DMN] Hamlib: Can't open rig", e = sys.exc_info()[0], error=e)
jsondata = json.dumps(response)
sock.SOCKET_QUEUE.put(jsondata)
except Exception as e:
structlog.get_logger("structlog").error("[DMN] Network error", error=e)
structlog.get_logger("structlog").warning("[DMN] Closing client socket", ip=self.client_address[0], port=self.client_address[1])
print(e)
if __name__ == '__main__':
# we need to run this on windows for multiprocessing support
multiprocessing.freeze_support()
# --------------------------------------------GET PARAMETER INPUTS
PARSER = argparse.ArgumentParser(description='Simons TEST TNC')
PARSER.add_argument('--port', dest="socket_port",default=3001, help="Socket port", type=int)
PARSER = argparse.ArgumentParser(description='FreeDATA Daemon')
PARSER.add_argument('--port', dest="socket_port",default=3001, help="Socket port in the range of 1024-65536", type=int)
ARGS = PARSER.parse_args()
PORT = ARGS.socket_port
# --------------------------------------------START CMD SERVER
static.DAEMONPORT = ARGS.socket_port
DAEMON_THREAD = threading.Thread(target=start_daemon, name="daemon")
DAEMON_THREAD.start()
try:
if sys.platform == 'linux':
logging_path = os.getenv("HOME") + '/.config/' + 'FreeDATA/' + 'daemon'
if sys.platform == 'darwin':
logging_path = os.getenv("HOME") + '/Library/' + 'Application Support/' + 'FreeDATA/' + 'daemon'
if sys.platform == 'win32' or sys.platform == 'win64':
logging_path = os.getenv('APPDATA') + '/' + 'FreeDATA/' + 'daemon'
if not os.path.exists(logging_path):
os.makedirs(logging_path)
log_handler.setup_logging(logging_path)
except:
structlog.get_logger("structlog").error("[DMN] logger init error")
try:
structlog.get_logger("structlog").info("[DMN] Starting TCP/IP socket", port=static.DAEMONPORT)
# https://stackoverflow.com/a/16641793
socketserver.TCPServer.allow_reuse_address = True
cmdserver = sock.ThreadedTCPServer((static.HOST, static.DAEMONPORT), sock.ThreadedTCPRequestHandler)
server_thread = threading.Thread(target=cmdserver.serve_forever)
server_thread.daemon = True
server_thread.start()
except Exception as e:
structlog.get_logger("structlog").error("[DMN] Starting TCP/IP socket failed", port=static.DAEMONPORT, e=e)
os._exit(1)
daemon = DAEMON()
structlog.get_logger("structlog").info("[DMN] Starting FreeDATA Daemon", author="DJ2LS", year="2022", version=static.VERSION)
while True:
time.sleep(1)

File diff suppressed because it is too large Load diff

View file

@ -24,7 +24,7 @@ daemon_exe = EXE(daemon_pyz,
daemon_a.scripts,
[],
exclude_binaries=True,
name='daemon',
name='freedata-daemon',
debug=False,
bootloader_ignore_signals=False,
strip=False,
@ -36,8 +36,8 @@ daemon_exe = EXE(daemon_pyz,
entitlements_file=None )
# add lib folder to system path. We only need to do this once
daemon_a.datas += Tree('./lib', prefix='lib')
daemon_a.datas += Tree('./codec2', prefix='codec2')
daemon_a.datas += Tree('lib', prefix='lib')
# daemon_a.datas += Tree('./codec2', prefix='codec2')
# TNC --------------------------------------------------
@ -61,7 +61,7 @@ tnc_exe = EXE(tnc_pyz,
tnc_a.scripts,
[],
exclude_binaries=True,
name='tnc',
name='freedata-tnc',
debug=False,
bootloader_ignore_signals=False,
strip=False,

View file

@ -13,6 +13,14 @@ import static
def wait(seconds):
"""
Args:
seconds:
Returns:
"""
timeout = time.time() + seconds
while time.time() < timeout:
@ -22,12 +30,17 @@ def wait(seconds):
def get_crc_8(data):
"""
Author: DJ2LS
"""Author: DJ2LS
Get the CRC8 of a byte string
param: data = bytes()
Args:
data:
Returns:
"""
crc_algorithm = crcengine.new('crc8-ccitt') # load crc8 library
crc_data = crc_algorithm(data)
@ -36,22 +49,57 @@ def get_crc_8(data):
def get_crc_16(data):
"""
Author: DJ2LS
"""Author: DJ2LS
Get the CRC16 of a byte string
param: data = bytes()
Args:
data:
Returns:
"""
crc_algorithm = crcengine.new('crc16-ccitt-false') # load crc16 library
crc_data = crc_algorithm(data)
crc_data = crc_data.to_bytes(2, byteorder='big')
return crc_data
def get_crc_32(data):
"""Author: DJ2LS
Get the CRC32 of a byte string
param: data = bytes()
Args:
data:
Returns:
"""
crc_algorithm = crcengine.new('crc32') # load crc16 library
crc_data = crc_algorithm(data)
crc_data = crc_data.to_bytes(4, byteorder='big')
return crc_data
def add_to_heard_stations(dxcallsign, dxgrid, datatype, snr, offset, frequency):
"""
Args:
dxcallsign:
dxgrid:
datatype:
snr:
offset:
frequency:
Returns:
"""
# check if buffer empty
if len(static.HEARD_STATIONS) == 0:
static.HEARD_STATIONS.append([dxcallsign, dxgrid, int(time.time()), datatype, snr, offset, frequency])
@ -73,31 +121,94 @@ def add_to_heard_stations(dxcallsign, dxgrid, datatype, snr, offset, frequency):
# item = [dxcallsign, int(time.time())]
# static.HEARD_STATIONS[idx] = item
'''
def setup_logging():
def callsign_to_bytes(callsign):
"""
Author: DJ2LS
Set the custom logging format so we can use colors
Args:
callsign:
# https://stackoverflow.com/questions/384076/how-can-i-color-python-logging-output
# 'DEBUG' : 37, # white
# 'INFO' : 36, # cyan
# 'WARNING' : 33, # yellow
# 'ERROR' : 31, # red
# 'CRITICAL': 41, # white on red bg
Returns:
"""
# http://www.aprs.org/aprs11/SSIDs.txt
#-0 Your primary station usually fixed and message capable
#-1 generic additional station, digi, mobile, wx, etc
#-2 generic additional station, digi, mobile, wx, etc
#-3 generic additional station, digi, mobile, wx, etc
#-4 generic additional station, digi, mobile, wx, etc
#-5 Other networks (Dstar, Iphones, Androids, Blackberry's etc)
#-6 Special activity, Satellite ops, camping or 6 meters, etc
#-7 walkie talkies, HT's or other human portable
#-8 boats, sailboats, RV's or second main mobile
#-9 Primary Mobile (usually message capable)
#-10 internet, Igates, echolink, winlink, AVRS, APRN, etc
#-11 balloons, aircraft, spacecraft, etc
#-12 APRStt, DTMF, RFID, devices, one-way trackers*, etc
#-13 Weather stations
#-14 Truckers or generally full time drivers
#-15 generic additional station, digi, mobile, wx, etc
# try converting to bytestring if possible type string
try:
callsign = bytes(callsign, 'utf-8')
except:
pass
# we need to do this step to reduce the needed paypload by the callsign ( stripping "-" out of the callsign )
callsign = callsign.split(b'-')
try:
ssid = int(callsign[1])
except:
ssid = 0
callsign = callsign[0]
bytestring = bytearray(8)
bytestring[:len(callsign)] = callsign
bytestring[7:8] = bytes([ssid])
return bytes(bytestring)
def bytes_to_callsign(bytestring):
"""
Args:
bytestring:
Returns:
"""
logging.basicConfig(level=logging.INFO,encoding='utf-8',format='%(asctime)s.%(msecs)03d %(levelname)s:\t%(message)s',datefmt='%H:%M:%S',handlers=[logging.FileHandler("FreeDATA-TNC.log"), logging.StreamHandler()])
# http://www.aprs.org/aprs11/SSIDs.txt
#-0 Your primary station usually fixed and message capable
#-1 generic additional station, digi, mobile, wx, etc
#-2 generic additional station, digi, mobile, wx, etc
#-3 generic additional station, digi, mobile, wx, etc
#-4 generic additional station, digi, mobile, wx, etc
#-5 Other networks (Dstar, Iphones, Androids, Blackberry's etc)
#-6 Special activity, Satellite ops, camping or 6 meters, etc
#-7 walkie talkies, HT's or other human portable
#-8 boats, sailboats, RV's or second main mobile
#-9 Primary Mobile (usually message capable)
#-10 internet, Igates, echolink, winlink, AVRS, APRN, etc
#-11 balloons, aircraft, spacecraft, etc
#-12 APRStt, DTMF, RFID, devices, one-way trackers*, etc
#-13 Weather stations
#-14 Truckers or generally full time drivers
#-15 generic additional station, digi, mobile, wx, etc
# we need to do this step to reduce the needed paypload by the callsign ( stripping "-" out of the callsign )
logging.addLevelName(logging.DEBUG, "\033[1;36m%s\033[1;0m" % logging.getLevelName(logging.DEBUG))
logging.addLevelName(logging.INFO, "\033[1;37m%s\033[1;0m" % logging.getLevelName(logging.INFO))
logging.addLevelName(logging.WARNING, "\033[1;33m%s\033[1;0m" % logging.getLevelName(logging.WARNING))
logging.addLevelName(logging.ERROR, "\033[1;31m%s\033[1;0m" % "FAILED")
#logging.addLevelName( logging.ERROR, "\033[1;31m%s\033[1;0m" % logging.getLevelName(logging.ERROR))
logging.addLevelName(logging.CRITICAL, "\033[1;41m%s\033[1;0m" % logging.getLevelName(logging.CRITICAL))
callsign = bytes(bytestring[:7])
callsign = callsign.rstrip(b'\x00')
ssid = int.from_bytes(bytes(bytestring[7:8]), "big")
callsign = callsign + b'-'
callsign = callsign.decode('utf-8')
callsign = callsign + str(ssid)
callsign = callsign.encode('utf-8')
return bytes(callsign)
logging.addLevelName(25, "\033[1;32m%s\033[1;0m" % "SUCCESS")
logging.addLevelName(24, "\033[1;34m%s\033[1;0m" % "DATA")
'''

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,5 +1,13 @@
# https://www.structlog.org/en/stable/standard-library.html
def setup_logging(filename):
"""
Args:
filename:
Returns:
"""
import logging.config
import structlog

108
tnc/main.py Normal file → Executable file
View file

@ -5,6 +5,9 @@ Created on Tue Dec 22 16:58:45 2020
@author: DJ2LS
main module for running the tnc
"""
@ -18,38 +21,66 @@ import structlog
import log_handler
import modem
import sys
import os
import signal
import time
import multiprocessing
# signal handler for closing aplication
def signal_handler(sig, frame):
"""
a signal handler, which closes the network/socket when closing the application
Args:
sig: signal
frame:
Returns: system exit
"""
print('Closing tnc...')
sock.CLOSE_SIGNAL = True
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
if __name__ == '__main__':
# we need to run this on windows for multiprocessing support
multiprocessing.freeze_support()
# --------------------------------------------GET PARAMETER INPUTS
PARSER = argparse.ArgumentParser(description='FreeDATA TNC')
PARSER.add_argument('--mycall', dest="mycall", default="AA0AA", help="My callsign", type=str)
PARSER.add_argument('--mygrid', dest="mygrid", default="JN12AA", help="My gridsquare", type=str)
PARSER.add_argument('--rx', dest="audio_input_device", default=0, help="listening sound card", type=int)
PARSER.add_argument('--tx', dest="audio_output_device", default=0, help="transmitting sound card", type=int)
PARSER.add_argument('--port', dest="socket_port", default=3000, help="Socket port", type=int)
PARSER.add_argument('--port', dest="socket_port", default=3000, help="Socket port in the range of 1024-65536", type=int)
PARSER.add_argument('--deviceport', dest="hamlib_device_port", default="/dev/ttyUSB0", help="Hamlib device port", type=str)
PARSER.add_argument('--devicename', dest="hamlib_device_name", default=2028, help="Hamlib device name", type=str)
PARSER.add_argument('--serialspeed', dest="hamlib_serialspeed", default=9600, help="Serialspeed", type=str)
PARSER.add_argument('--pttprotocol', dest="hamlib_ptt_type", default='RTS', help="PTT Type", type=str)
PARSER.add_argument('--devicename', dest="hamlib_device_name", default="2028", help="Hamlib device name", type=str)
PARSER.add_argument('--serialspeed', dest="hamlib_serialspeed", choices=[1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200], default=9600, help="Serialspeed", type=int)
PARSER.add_argument('--pttprotocol', dest="hamlib_ptt_type", choices=['USB', 'RIG', 'RTS', 'DTR', 'CM108', 'MICDATA', 'PARALLEL', 'DTR-H', 'DTR-L', 'NONE'], default='USB', help="PTT Type", type=str)
PARSER.add_argument('--pttport', dest="hamlib_ptt_port", default="/dev/ttyUSB0", help="PTT Port", type=str)
PARSER.add_argument('--data_bits', dest="hamlib_data_bits", default="8", help="Hamlib data bits", type=str)
PARSER.add_argument('--stop_bits', dest="hamlib_stop_bits", default="1", help="Hamlib stop bits", type=str)
PARSER.add_argument('--data_bits', dest="hamlib_data_bits", choices=[7, 8], default=8, help="Hamlib data bits", type=int)
PARSER.add_argument('--stop_bits', dest="hamlib_stop_bits", choices=[1, 2], default=1, help="Hamlib stop bits", type=int)
PARSER.add_argument('--handshake', dest="hamlib_handshake", default="None", help="Hamlib handshake", type=str)
PARSER.add_argument('--radiocontrol', dest="hamlib_radiocontrol", default="direct", help="Set how you want to control your radio")
PARSER.add_argument('--rigctld_port', dest="rigctld_port", default="direct", help="Set rigctld port")
PARSER.add_argument('--rigctld_ip', dest="rigctld_ip", default="direct", help="Set rigctld ip")
PARSER.add_argument('--radiocontrol', dest="hamlib_radiocontrol", choices=['disabled', 'direct', 'rigctl', 'rigctld'], default="disabled", help="Set how you want to control your radio")
PARSER.add_argument('--rigctld_port', dest="rigctld_port", default=4532, type=int, help="Set rigctld port")
PARSER.add_argument('--rigctld_ip', dest="rigctld_ip", default="localhost", help="Set rigctld ip")
PARSER.add_argument('--scatter', dest="send_scatter", action="store_true", help="Send scatter information via network")
PARSER.add_argument('--fft', dest="send_fft", action="store_true", help="Send fft information via network")
PARSER.add_argument('--500hz', dest="low_bandwith_mode", action="store_true", help="Enable low bandwith mode ( 500 Hz only )")
ARGS = PARSER.parse_args()
static.MYCALLSIGN = bytes(ARGS.mycall, 'utf-8')
static.MYCALLSIGN_CRC8 = helpers.get_crc_8(static.MYCALLSIGN)
# 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
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_16(static.MYCALLSIGN)
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
@ -57,26 +88,52 @@ if __name__ == '__main__':
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 = ARGS.hamlib_serialspeed
static.HAMLIB_DATA_BITS = ARGS.hamlib_data_bits
static.HAMLIB_STOP_BITS = ARGS.hamlib_stop_bits
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_RGICTLD_IP = ARGS.rigctld_ip
static.HAMLIB_RGICTLD_PORT = ARGS.rigctld_port
static.HAMLIB_RGICTLD_PORT = str(ARGS.rigctld_port)
static.ENABLE_SCATTER = ARGS.send_scatter
static.ENABLE_FFT = ARGS.send_fft
static.LOW_BANDWITH_MODE = ARGS.low_bandwith_mode
# we need to wait until we got all parameters from argparse first before we can load the other modules
import sock
# config logging
log_handler.setup_logging("tnc")
structlog.get_logger("structlog").info("[TNC] Starting FreeDATA", author="DJ2LS", year="2022", version="0.1")
# config logging
try:
if sys.platform == 'linux':
logging_path = os.getenv("HOME") + '/.config/' + 'FreeDATA/' + 'tnc'
if sys.platform == 'darwin':
logging_path = os.getenv("HOME") + '/Library/' + 'Application Support/' + 'FreeDATA/' + 'tnc'
if sys.platform == 'win32' or sys.platform == 'win64':
logging_path = os.getenv('APPDATA') + '/' + 'FreeDATA/' + 'tnc'
if not os.path.exists(logging_path):
os.makedirs(logging_path)
log_handler.setup_logging(logging_path)
except:
structlog.get_logger("structlog").error("[DMN] logger init error")
structlog.get_logger("structlog").info("[TNC] Starting FreeDATA", author="DJ2LS", year="2022", version=static.VERSION)
# start data handler
data_handler.DATA()
# start modem
modem = modem.RF()
# --------------------------------------------START CMD SERVER
try:
@ -85,9 +142,12 @@ if __name__ == '__main__':
socketserver.TCPServer.allow_reuse_address = True
cmdserver = sock.ThreadedTCPServer((static.HOST, static.PORT), sock.ThreadedTCPRequestHandler)
server_thread = threading.Thread(target=cmdserver.serve_forever)
server_thread.daemon = True
server_thread.start()
except Exception as e:
structlog.get_logger("structlog").error("[TNC] Starting TCP/IP socket failed", port=static.PORT, e=e)
os._exit(1)
while 1:
time.sleep(1)

View file

@ -6,10 +6,10 @@ Created on Wed Dec 23 07:04:24 2020
@author: DJ2LS
"""
import sys
import os
import ctypes
from ctypes import *
import pathlib
#import asyncio
import logging, structlog, log_handler
import time
import threading
@ -18,46 +18,21 @@ import numpy as np
import helpers
import static
import data_handler
import ujson as json
import sock
import re
import queue
import codec2
import audio
####################################################
# https://stackoverflow.com/questions/7088672/pyaudio-working-but-spits-out-error-messages-each-time
# https://github.com/DJ2LS/FreeDATA/issues/22
# we need to have a look at this if we want to run this on Windows and MacOS !
# Currently it seems, this is a Linux-only problem
from ctypes import *
from contextlib import contextmanager
import pyaudio
ERROR_HANDLER_FUNC = CFUNCTYPE(None, c_char_p, c_int, c_char_p, c_int, c_char_p)
def py_error_handler(filename, line, function, err, fmt):
pass
c_error_handler = ERROR_HANDLER_FUNC(py_error_handler)
@contextmanager
def noalsaerr():
asound = cdll.LoadLibrary('libasound.so')
asound.snd_lib_error_set_handler(c_error_handler)
yield
asound.snd_lib_error_set_handler(None)
# with noalsaerr():
# p = pyaudio.PyAudio()
######################################################
from collections import deque
MODEM_STATS_NR_MAX = 320
MODEM_STATS_NC_MAX = 51
class MODEMSTATS(ctypes.Structure):
""" """
_fields_ = [
("Nc", ctypes.c_int),
("snr_est", ctypes.c_float),
@ -83,8 +58,14 @@ RECEIVE_DATAC1 = False
RECEIVE_DATAC3 = False
class RF():
""" """
def __init__(self):
self.sampler_avg = 0
self.buffer_avg = 0
self.AUDIO_SAMPLE_RATE_RX = 48000
self.AUDIO_SAMPLE_RATE_TX = 48000
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
@ -93,6 +74,11 @@ class RF():
self.AUDIO_CHUNKS = 48 #8 * (self.AUDIO_SAMPLE_RATE_RX/self.MODEM_SAMPLE_RATE) #48
self.AUDIO_CHANNELS = 1
# 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
self.mod_out_locked = True
# make sure our resampler will work
assert (self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
@ -105,13 +91,16 @@ class RF():
self.modem_received_queue = MODEM_RECEIVED_QUEUE
# init FIFO queue to store modulation out in
self.modoutqueue = queue.Queue()
self.modoutqueue = deque()
# define fft_data buffer
self.fft_data = bytes()
# open codec2 instance
self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p)
#self.c_lib.freedv_set_tuning_range(self.datac0_freedv, c_float(-150.0), c_float(150.0))
self.datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv)/8)
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)
@ -143,13 +132,13 @@ class RF():
try:
# we need to "try" this, because sometimes libasound.so isn't in the default place
# try to supress error messages
with noalsaerr(): # https://github.com/DJ2LS/FreeDATA/issues/22
self.p = pyaudio.PyAudio()
with audio.noalsaerr(): # https://github.com/DJ2LS/FreeDATA/issues/22
self.p = audio.pyaudio.PyAudio()
# else do it the default way
except:
self.p = pyaudio.PyAudio()
self.p = audio.pyaudio.PyAudio()
atexit.register(self.p.terminate)
# --------------------------------------------OPEN RX AUDIO CHANNEL
# optional auto selection of loopback device if using in testmode
if static.AUDIO_INPUT_DEVICE == -2:
@ -161,20 +150,28 @@ class RF():
static.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX
static.AUDIO_OUTPUT_DEVICE = loopback_list[1] #1 = TX
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
self.audio_stream = self.p.open(format=pyaudio.paInt16,
channels=self.AUDIO_CHANNELS,
rate=self.AUDIO_SAMPLE_RATE_RX,
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER_RX,
input=True,
output=True,
input_device_index=static.AUDIO_INPUT_DEVICE,
output_device_index=static.AUDIO_OUTPUT_DEVICE,
stream_callback=self.audio_callback
)
try:
self.audio_stream = self.p.open(format=audio.pyaudio.paInt16,
channels=self.AUDIO_CHANNELS,
rate=self.AUDIO_SAMPLE_RATE_RX,
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER_RX,
input=True,
output=True,
input_device_index=static.AUDIO_INPUT_DEVICE,
output_device_index=static.AUDIO_OUTPUT_DEVICE,
stream_callback=self.audio_callback
)
structlog.get_logger("structlog").info("opened audio devices")
except Exception as e:
structlog.get_logger("structlog").error("can't open audio device. Exit", e=e)
os._exit(1)
try:
structlog.get_logger("structlog").debug("[TNC] starting pyaudio callback")
self.audio_stream.start_stream()
except Exception as e:
structlog.get_logger("structlog").error("[TNC] starting pyaudio callback failed", e=e)
@ -186,78 +183,112 @@ class RF():
import rigctl as rig
elif static.HAMLIB_RADIOCONTROL == 'rigctld':
import rigctld as rig
elif static.HAMLIB_RADIOCONTROL == 'disabled':
import rigdummy as rig
else:
raise NotImplementedError
import rigdummy as rig
self.hamlib = rig.radio()
self.hamlib.open_rig(devicename=static.HAMLIB_DEVICE_NAME, deviceport=static.HAMLIB_DEVICE_PORT, hamlib_ptt_type=static.HAMLIB_PTT_TYPE, serialspeed=static.HAMLIB_SERIAL_SPEED, pttport=static.HAMLIB_PTT_PORT, data_bits=static.HAMLIB_DATA_BITS, stop_bits=static.HAMLIB_STOP_BITS, handshake=static.HAMLIB_HANDSHAKE, rigctld_ip = static.HAMLIB_RGICTLD_IP, rigctld_port = static.HAMLIB_RGICTLD_PORT)
# --------------------------------------------START DECODER THREAD
fft_thread = threading.Thread(target=self.calculate_fft, name="FFT_THREAD")
fft_thread.start()
if static.ENABLE_FFT:
fft_thread = threading.Thread(target=self.calculate_fft, name="FFT_THREAD" ,daemon=True)
fft_thread.start()
audio_thread_datac0 = threading.Thread(target=self.audio_datac0, name="AUDIO_THREAD DATAC0")
audio_thread_datac0 = threading.Thread(target=self.audio_datac0, name="AUDIO_THREAD DATAC0",daemon=True)
audio_thread_datac0.start()
audio_thread_datac1 = threading.Thread(target=self.audio_datac1, name="AUDIO_THREAD DATAC1")
audio_thread_datac1 = threading.Thread(target=self.audio_datac1, name="AUDIO_THREAD DATAC1",daemon=True)
audio_thread_datac1.start()
audio_thread_datac3 = threading.Thread(target=self.audio_datac3, name="AUDIO_THREAD DATAC3")
audio_thread_datac3 = threading.Thread(target=self.audio_datac3, name="AUDIO_THREAD DATAC3",daemon=True)
audio_thread_datac3.start()
hamlib_thread = threading.Thread(target=self.update_rig_data, name="HAMLIB_THREAD")
hamlib_thread = threading.Thread(target=self.update_rig_data, name="HAMLIB_THREAD",daemon=True)
hamlib_thread.start()
worker_received = threading.Thread(target=self.worker_received, name="WORKER_THREAD")
worker_received = threading.Thread(target=self.worker_received, name="WORKER_THREAD",daemon=True)
worker_received.start()
worker_transmit = threading.Thread(target=self.worker_transmit, name="WORKER_THREAD")
worker_transmit = threading.Thread(target=self.worker_transmit, name="WORKER_THREAD",daemon=True)
worker_transmit.start()
# --------------------------------------------------------------------------------------------------------
def audio_callback(self, data_in48k, frame_count, time_info, status):
"""
Args:
data_in48k:
frame_count:
time_info:
status:
Returns:
"""
x = np.frombuffer(data_in48k, dtype=np.int16)
x = self.resampler.resample48_to_8(x)
# avoid buffer overflow by filling only if buffer not full
if not self.datac0_buffer.nbuffer+len(x) > self.datac0_buffer.size:
self.datac0_buffer.push(x)
else:
static.BUFFER_OVERFLOW_COUNTER[0] += 1
# avoid buffer overflow by filling only if buffer not full and selected datachannel mode
if not self.datac1_buffer.nbuffer+len(x) > self.datac1_buffer.size:
if RECEIVE_DATAC1:
self.datac1_buffer.push(x)
else:
static.BUFFER_OVERFLOW_COUNTER[1] += 1
# avoid buffer overflow by filling only if buffer not full and selected datachannel mode
if not self.datac3_buffer.nbuffer+len(x) > self.datac3_buffer.size:
if RECEIVE_DATAC3:
self.datac3_buffer.push(x)
else:
static.BUFFER_OVERFLOW_COUNTER[2] += 1
if self.modoutqueue.empty():
data_out48k = bytes(self.AUDIO_FRAMES_PER_BUFFER_TX*2)
length_x = len(x)
# avoid decoding when transmitting to reduce CPU
if not static.TRANSMITTING:
# avoid buffer overflow by filling only if buffer not full
if not self.datac0_buffer.nbuffer+length_x > self.datac0_buffer.size:
self.datac0_buffer.push(x)
else:
static.BUFFER_OVERFLOW_COUNTER[0] += 1
# avoid buffer overflow by filling only if buffer not full and selected datachannel mode
if not self.datac1_buffer.nbuffer+length_x > self.datac1_buffer.size:
if RECEIVE_DATAC1:
self.datac1_buffer.push(x)
else:
static.BUFFER_OVERFLOW_COUNTER[1] += 1
# avoid buffer overflow by filling only if buffer not full and selected datachannel mode
if not self.datac3_buffer.nbuffer+length_x > self.datac3_buffer.size:
if RECEIVE_DATAC3:
self.datac3_buffer.push(x)
else:
static.BUFFER_OVERFLOW_COUNTER[2] += 1
if not self.modoutqueue or self.mod_out_locked:
data_out48k = np.zeros(frame_count, dtype=np.int16)
self.fft_data = bytes(x)
else:
data_out48k = self.modoutqueue.get()
data_out48k = self.modoutqueue.popleft()
self.fft_data = bytes(data_out48k)
return (data_out48k, pyaudio.paContinue)
return (data_out48k, audio.pyaudio.paContinue)
# --------------------------------------------------------------------------------------------------------
def transmit(self, mode, repeats, repeat_delay, frames):
"""
Args:
mode:
repeats:
repeat_delay:
frames:
Returns:
"""
static.TRANSMITTING = True
# toggle ptt early to save some time
# 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)
# open codec2 instance
self.MODE = mode
freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p)
@ -287,11 +318,8 @@ class RF():
for i in range(1,repeats+1):
# write preamble to txbuffer
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
#time.sleep(0.05)
threading.Event().wait(0.05)
txbuffer += bytes(mod_out_preamble)
# create modulaton for n frames in list
for n in range(0,len(frames)):
@ -307,52 +335,58 @@ class RF():
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
codec2.api.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and save it into mod_out pointer
#time.sleep(0.05)
threading.Event().wait(0.05)
txbuffer += bytes(mod_out)
# append postamble to txbuffer
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
txbuffer += bytes(mod_out_postamble)
#time.sleep(0.05)
threading.Event().wait(0.05)
# add delay to end of frames
samples_delay = int(self.MODEM_SAMPLE_RATE*(repeat_delay/1000))
mod_out_silence = create_string_buffer(samples_delay*2)
txbuffer += bytes(mod_out_silence)
#time.sleep(0.05)
# resample up to 48k (resampler works on np.int16)
x = np.frombuffer(txbuffer, dtype=np.int16)
txbuffer_48k = self.resampler.resample8_to_48(x)
# split modualted audio to chunks
#https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python
txbuffer_48k = bytes(txbuffer_48k)
chunk = [txbuffer_48k[i:i+self.AUDIO_FRAMES_PER_BUFFER_RX*2] for i in range(0, len(txbuffer_48k), self.AUDIO_FRAMES_PER_BUFFER_RX*2)]
# add modulated chunks to fifo buffer
for c in chunk:
# if data is shorter than the expcected audio frames per buffer we need to append 0
# to prevent the callback from stucking/crashing
if len(c) < self.AUDIO_FRAMES_PER_BUFFER_RX*2:
delta = bytes(self.AUDIO_FRAMES_PER_BUFFER_RX*2 - len(c))
c += delta
structlog.get_logger("structlog").debug("[TNC] mod out shorter than audio buffer", delta=len(delta))
self.modoutqueue.put(c)
# explicitly lock our usage of mod_out_queue if needed
# deaktivated for testing purposes
self.mod_out_locked = False
# maybe we need to toggle PTT before craeting modulation because of queue processing
#static.PTT_STATE = self.hamlib.set_ptt(True)
while not self.modoutqueue.empty():
pass
chunk_length = self.AUDIO_FRAMES_PER_BUFFER_TX #4800
chunk = [txbuffer_48k[i:i+chunk_length] for i in range(0, len(txbuffer_48k), chunk_length)]
for c in chunk:
if len(c) < chunk_length:
delta = chunk_length - len(c)
delta_zeros = np.zeros(delta, dtype=np.int16)
c = np.append(c, delta_zeros)
#structlog.get_logger("structlog").debug("[TNC] mod out shorter than audio buffer", delta=delta)
self.modoutqueue.append(c)
# Release our mod_out_lock so we can use the queue
self.mod_out_locked = False
while self.modoutqueue:
time.sleep(0.01)
static.PTT_STATE = self.hamlib.set_ptt(False)
# push ptt state to socket stream
jsondata = {"ptt":"False"}
data_out = json.dumps(jsondata)
sock.SOCKET_QUEUE.put(data_out)
# after processing we want to 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()
def audio_datac0(self):
""" """
nbytes_datac0 = 0
while self.audio_stream.is_active():
threading.Event().wait(0.01)
@ -367,20 +401,22 @@ class RF():
self.calculate_snr(self.datac0_freedv)
def audio_datac1(self):
""" """
nbytes_datac1 = 0
while self.audio_stream.is_active():
threading.Event().wait(0.01)
while self.datac1_buffer.nbuffer >= self.datac1_nin:
# demodulate audio
nbytes_datac1 = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes)
self.datac1_buffer.pop(self.datac1_nin)
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
if nbytes_datac1 == self.datac1_bytes_per_frame:
self.modem_received_queue.put([self.datac1_bytes_out, self.datac1_freedv ,self.datac1_bytes_per_frame])
self.get_scatter(self.datac1_freedv)
self.calculate_snr(self.datac1_freedv)
# demodulate audio
nbytes_datac1 = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes)
self.datac1_buffer.pop(self.datac1_nin)
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
if nbytes_datac1 == self.datac1_bytes_per_frame:
self.modem_received_queue.put([self.datac1_bytes_out, self.datac1_freedv ,self.datac1_bytes_per_frame])
self.get_scatter(self.datac1_freedv)
self.calculate_snr(self.datac1_freedv)
def audio_datac3(self):
""" """
nbytes_datac3 = 0
while self.audio_stream.is_active():
threading.Event().wait(0.01)
@ -398,6 +434,7 @@ class RF():
# worker for FIFO queue for processing received frames
def worker_transmit(self):
""" """
while True:
data = self.modem_transmit_queue.get()
self.transmit(mode=data[0], repeats=data[1], repeat_delay=data[2], frames=data[3])
@ -407,6 +444,7 @@ class RF():
# worker for FIFO queue for processing received frames
def worker_received(self):
""" """
while True:
data = self.modem_received_queue.get()
# data[0] = bytes_out
@ -417,6 +455,14 @@ class RF():
def get_frequency_offset(self, freedv):
"""
Args:
freedv:
Returns:
"""
modemStats = MODEMSTATS()
self.c_lib.freedv_get_modem_extended_stats.restype = None
self.c_lib.freedv_get_modem_extended_stats(freedv, ctypes.byref(modemStats))
@ -426,32 +472,49 @@ class RF():
def get_scatter(self, freedv):
modemStats = MODEMSTATS()
self.c_lib.freedv_get_modem_extended_stats.restype = None
self.c_lib.freedv_get_modem_extended_stats(freedv, ctypes.byref(modemStats))
"""
scatterdata = []
scatterdata_small = []
for i in range(MODEM_STATS_NC_MAX):
for j in range(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})
Args:
freedv:
# only append scatter data if new data arrived
if 150 > len(scatterdata) > 0:
static.SCATTER = scatterdata
else:
# only take every tenth data point
scatterdata_small = scatterdata[::10]
static.SCATTER = scatterdata_small
Returns:
"""
if static.ENABLE_SCATTER:
modemStats = MODEMSTATS()
self.c_lib.freedv_get_modem_extended_stats.restype = None
self.c_lib.freedv_get_modem_extended_stats(freedv, ctypes.byref(modemStats))
scatterdata = []
scatterdata_small = []
for i in range(MODEM_STATS_NC_MAX):
for j in range(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})
# only append scatter data if new data arrived
if 150 > len(scatterdata) > 0:
static.SCATTER = scatterdata
else:
# only take every tenth data point
scatterdata_small = scatterdata[::10]
static.SCATTER = scatterdata_small
def calculate_snr(self, freedv):
"""
Args:
freedv:
Returns:
"""
modem_stats_snr = c_float()
modem_stats_sync = c_int()
@ -468,8 +531,9 @@ class RF():
return static.SNR
def update_rig_data(self):
""" """
while True:
#time.sleep(0.5)
#time.sleep(1.5)
threading.Event().wait(0.5)
#(static.HAMLIB_FREQUENCY, static.HAMLIB_MODE, static.HAMLIB_BANDWITH, static.PTT_STATE) = self.hamlib.get_rig_data()
static.HAMLIB_FREQUENCY = self.hamlib.get_frequency()
@ -477,6 +541,10 @@ class RF():
static.HAMLIB_BANDWITH = self.hamlib.get_bandwith()
def calculate_fft(self):
""" """
# channel_busy_delay counter
channel_busy_delay = 0
while True:
#time.sleep(0.01)
threading.Event().wait(0.01)
@ -484,8 +552,9 @@ class RF():
if len(self.fft_data) >= 128:
data_in = self.fft_data
# delte fft_buffer
self.fft_data = bytes()
# delete fft_buffer
self.fft_data = bytes()
# https://gist.github.com/ZWMiller/53232427efc5088007cab6feee7c6e4c
audio_data = np.fromstring(data_in, np.int16)
@ -498,8 +567,35 @@ class RF():
# set value 0 to 1 to avoid division by zero
fftarray[fftarray == 0] = 1
dfft = 10.*np.log10(abs(fftarray))
# get average of dfft
avg = np.mean(dfft)
# detect signals which are higher than the average + 10 ( +10 smoothes the output )
# data higher than the average must be a signal. Therefore we are setting it to 100 so it will be highlighted
# have to do this when we are not transmittig so our own sending data will not affect this too much
if not static.TRANSMITTING:
dfft[dfft>avg+10] = 100
# 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:
static.CHANNEL_BUSY = True
channel_busy_delay += 5
# limit delay counter to a maximun of 30. The higher this value, the linger we will wait until releasing state
if channel_busy_delay > 50:
channel_busy_delay = 50
else:
# decrement channel busy counter if no signal has been detected.
channel_busy_delay -= 1
if channel_busy_delay < 0:
channel_busy_delay = 0
# if our channel busy counter reached 0, we toggle state to False
if channel_busy_delay == 0:
static.CHANNEL_BUSY = False
# round data to decrease size
dfft = np.around(dfft, 1)
dfft = np.around(dfft, 0)
dfftlist = dfft.tolist()
static.FFT = dfftlist[0:320] #200 --> bandwith 3000
@ -508,11 +604,19 @@ class RF():
structlog.get_logger("structlog").debug("[TNC] Setting fft=0")
# else 0
static.FFT = [0] * 320
static.FFT = [0]
else:
pass
def set_frames_per_burst(self, n_frames_per_burst):
"""
Args:
n_frames_per_burst:
Returns:
"""
codec2.api.freedv_set_frames_per_burst(self.datac1_freedv,n_frames_per_burst)
codec2.api.freedv_set_frames_per_burst(self.datac3_freedv,n_frames_per_burst)
@ -520,6 +624,14 @@ class RF():
def get_bytes_per_frame(mode):
"""
Args:
mode:
Returns:
"""
freedv = cast(codec2.api.freedv_open(mode), c_void_p)
# get number of bytes per frame for mode

View file

@ -63,7 +63,7 @@ except Exception as e:
hamlib_version = hamlib_version.split(' ')
if hamlib_version[1] == 'Hamlib':
structlog.get_logger("structlog").warning("[RIG] Rigctl found! Start daemon with parameter --rigctl", version=hamlib_version[2])
structlog.get_logger("structlog").warning("[RIG] Rigctl found! Please try using this", version=hamlib_version[2])
sys.exit()
else:
raise Exception
@ -74,6 +74,7 @@ except Exception as e:
class radio:
""" """
def __init__(self):
self.devicename = ''
@ -88,6 +89,23 @@ class radio:
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:
Returns:
"""
self.devicename = devicename
self.deviceport = str(deviceport)
@ -106,7 +124,6 @@ class radio:
# get devicenumber by looking for deviceobject in Hamlib module
try:
self.devicenumber = int(getattr(Hamlib, self.devicename))
print(self.devicenumber)
except:
structlog.get_logger("structlog").error("[RIG] Hamlib: rig not supported...")
self.devicenumber = 0
@ -120,17 +137,9 @@ class radio:
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)
print(self.my_rig.get_conf("rig_pathname"))
print(self.my_rig.get_conf("retry"))
print(self.my_rig.get_conf("serial_speed"))
print(self.my_rig.get_conf("serial_handshake"))
print(self.my_rig.get_conf("stop_bits"))
print(self.my_rig.get_conf("data_bits"))
print(self.my_rig.get_conf("ptt_pathname"))
if self.hamlib_ptt_type == 'RIG':
self.hamlib_ptt_type = Hamlib.RIG_PTT_RIG
@ -165,10 +174,15 @@ class radio:
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
structlog.get_logger("structlog").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)
@ -200,13 +214,16 @@ class radio:
return False
def get_frequency(self):
""" """
return int(self.my_rig.get_freq())
def get_mode(self):
""" """
(hamlib_mode, bandwith) = self.my_rig.get_mode()
return Hamlib.rig_strrmode(hamlib_mode)
def get_bandwith(self):
""" """
(hamlib_mode, bandwith) = self.my_rig.get_mode()
return bandwith
@ -215,9 +232,18 @@ class radio:
# 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:
@ -225,4 +251,5 @@ class radio:
return state
def close_rig(self):
""" """
self.my_rig.close()

View file

@ -19,6 +19,7 @@ import os
hamlib_version = 0
class radio:
""" """
def __init__(self):
self.devicename = ''
@ -33,6 +34,23 @@ class radio:
self.handshake = ''
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
@ -87,35 +105,71 @@ class radio:
return True
def get_frequency(self):
""" """
cmd = 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())
return int(freq)
try:
return int(freq)
except:
return False
def get_mode(self):
""" """
#(hamlib_mode, bandwith) = self.my_rig.get_mode()
#return Hamlib.rig_strrmode(hamlib_mode)
return 'PKTUSB'
try:
return 'PKTUSB'
except:
return False
def get_bandwith(self):
""" """
#(hamlib_mode, bandwith) = self.my_rig.get_mode()
bandwith = 2700
return bandwith
try:
return bandwith
except:
return False
def set_mode(self, mode):
"""
Args:
mode:
Returns:
"""
# non usata
return 0
def get_ptt(self):
""" """
cmd = 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]
return status
try:
return status
except:
return False
def set_ptt(self, state):
"""
Args:
state:
Returns:
"""
cmd = self.cmd + ' T '
print('set_ptt', state)
if state:
@ -125,8 +179,12 @@ class radio:
print('set_ptt', cmd)
sw_proc = subprocess.Popen(cmd, shell=True, text=True)
return state
try:
return state
except:
return False
def close_rig(self):
""" """
#self.my_rig.close()
return

View file

@ -1,6 +1,10 @@
#!/usr/bin/env python3
import socket
import structlog
import log_handler
import logging
import time
import static
# class taken from darsidelemm
# rigctl - https://github.com/darksidelemm/rotctld-web-gui/blob/master/rotatorgui.py#L35
#
@ -10,72 +14,145 @@ import logging
hamlib_version = 0
class radio():
""" rotctld (hamlib) communication class """
"""rotctld (hamlib) communication class"""
# Note: This is a massive hack.
def __init__(self, hostname="localhost", port=4532, poll_rate=5, timeout=5):
""" Open a connection to rotctld, and test it for validity """
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(timeout)
#self.sock.settimeout(timeout)
self.connected = False
self.hostname = hostname
self.port = port
self.connection_attempts = 5
def open_rig(self, devicename, deviceport, hamlib_ptt_type, serialspeed, pttport, data_bits, stop_bits, handshake, rigctld_ip, rigctld_port):
self.connect()
logging.debug(f"Rigctl intialized")
return True
def connect(self):
""" Connect to rotctld instance """
self.sock.connect((self.hostname,self.port))
ptt = self.get_ptt()
if ptt == None:
# Timeout!
self.close()
raise Exception("Timeout!")
"""
Args:
devicename:
deviceport:
hamlib_ptt_type:
serialspeed:
pttport:
data_bits:
stop_bits:
handshake:
rigctld_ip:
rigctld_port:
Returns:
"""
self.hostname = rigctld_ip
self.port = int(rigctld_port)
if self.connect():
logging.debug(f"Rigctl intialized")
return True
else:
return ptt
structlog.get_logger("structlog").error("[RIGCTLD] Can't connect to rigctld!", ip=self.hostname, port=self.port)
return False
def connect(self):
"""Connect to rigctld instance"""
if not self.connected:
try:
self.connection = socket.create_connection((self.hostname,self.port))
self.connected = True
structlog.get_logger("structlog").info("[RIGCTLD] Connected to rigctld!", ip=self.hostname, port=self.port)
return True
except Exception as e:
# ConnectionRefusedError: [Errno 111] Connection refused
self.close_rig()
structlog.get_logger("structlog").warning("[RIGCTLD] Connection to rigctld refused! Reconnect...", ip=self.hostname, port=self.port, e=e)
return False
def close_rig(self):
""" """
self.sock.close()
self.connected = False
def send_command(self, command):
""" Send a command to the connected rotctld instance,
"""Send a command to the connected rotctld instance,
and return the return value.
Args:
command:
Returns:
"""
self.sock.sendall(command+b'\n')
try:
return self.sock.recv(1024)
except:
return None
if self.connected:
try:
self.connection.sendall(command+b'\n')
except:
structlog.get_logger("structlog").warning("[RIGCTLD] Command not executed!", command=command, ip=self.hostname, port=self.port)
self.connected = False
try:
return self.connection.recv(1024)
except:
structlog.get_logger("structlog").warning("[RIGCTLD] No command response!", command=command, ip=self.hostname, port=self.port)
self.connected = False
else:
# reconnecting....
time.sleep(0.5)
self.connect()
def get_mode(self):
data = self.send_command(b"m")
data = data.split(b'\n')
mode = data[0]
return mode.decode("utf-8")
""" """
try:
data = self.send_command(b"m")
data = data.split(b'\n')
mode = data[0]
return mode.decode("utf-8")
except:
0
def get_bandwith(self):
data = self.send_command(b"m")
data = data.split(b'\n')
bandwith = data[1]
return bandwith.decode("utf-8")
def get_frequency(self):
frequency = self.send_command(b"f")
return frequency.decode("utf-8")
def get_ptt(self):
return self.send_command(b"t")
def set_ptt(self, state):
if state:
self.send_command(b"T 1")
else:
self.send_command(b"T 0")
return state
""" """
try:
data = self.send_command(b"m")
data = data.split(b'\n')
bandwith = data[1]
return bandwith.decode("utf-8")
except:
return 0
def get_frequency(self):
""" """
try:
frequency = self.send_command(b"f")
return frequency.decode("utf-8")
except:
return 0
def get_ptt(self):
""" """
try:
return self.send_command(b"t")
except:
return False
def set_ptt(self, state):
"""
Args:
state:
Returns:
"""
try:
if state:
self.send_command(b"T 1")
else:
self.send_command(b"T 0")
return state
except:
return False

65
tnc/rigdummy.py Normal file
View file

@ -0,0 +1,65 @@
#!/usr/bin/env python3
import structlog
hamlib_version = 0
class radio:
""" """
def __init__(self):
pass
def open_rig(self, **kwargs):
"""
Args:
**kwargs:
Returns:
"""
return True
def get_frequency(self):
""" """
return None
def get_mode(self):
""" """
return None
def get_bandwith(self):
""" """
return None
def set_mode(self, mode):
"""
Args:
mode:
Returns:
"""
return None
def get_ptt(self):
""" """
return None
def set_ptt(self, state):
"""
Args:
state:
Returns:
"""
return state
def close_rig(self):
""" """
return

View file

@ -18,9 +18,7 @@ Created on Fri Dec 25 21:25:14 2020
# "dxcallsign" : "..."
# "data" : "..."
"""
import socketserver
import threading
import ujson as json
@ -31,252 +29,566 @@ import helpers
import sys
import os
import logging, structlog, log_handler
import queue
import psutil
import audio
import base64
import atexit
SOCKET_QUEUE = queue.Queue()
DAEMON_QUEUE = queue.Queue()
CONNECTED_CLIENTS = set()
CLOSE_SIGNAL = False
class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
"""
the socket handler base class
"""
pass
class ThreadedTCPRequestHandler(socketserver.StreamRequestHandler):
""" """
class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):
def handle(self):
structlog.get_logger("structlog").debug("[TNC] Client connected", ip=self.client_address[0], port=self.client_address[1])
# set encoding
encoding = 'utf-8'
# loop through socket buffer until timeout is reached. then close buffer
socketTimeout = time.time() + static.SOCKET_TIMEOUT
while socketTimeout > time.time():
time.sleep(0.01)
data = bytes()
# we need to loop through buffer until end of chunk is reached or timeout occured
while socketTimeout > time.time():
data += self.request.recv(64) # we keep amount of bytes short
if data.startswith(b'{"type"') and data.endswith(b'}\n'):
break
data = data[:-1] # remove b'\n'
data = str(data, encoding)
if len(data) > 0:
# reset socket timeout
socketTimeout = time.time() + static.SOCKET_TIMEOUT
# only read first line of string. multiple lines will cause an json error
# this occurs possibly, if we are getting data too fast
# data = data.splitlines()[0]
data = data.splitlines()[0]
# we need to do some error handling in case of socket timeout or decoding issue
try:
# convert data to json object
received_json = json.loads(data)
# CQ CQ CQ -----------------------------------------------------
if received_json["command"] == "CQCQCQ":
data_handler.DATA_QUEUE_TRANSMIT.put(['CQ'])
# START_BEACON -----------------------------------------------------
if received_json["command"] == "START_BEACON":
static.BEACON_STATE = True
interval = int(received_json["parameter"])
data_handler.DATA_QUEUE_TRANSMIT.put(['BEACON', interval, True])
# STOP_BEACON -----------------------------------------------------
if received_json["command"] == "STOP_BEACON":
static.BEACON_STATE = False
structlog.get_logger("structlog").warning("[TNC] Stopping beacon!")
data_handler.DATA_QUEUE_TRANSMIT.put(['BEACON', interval, False])
# PING ----------------------------------------------------------
if received_json["type"] == 'PING' and received_json["command"] == "PING":
# send ping frame and wait for ACK
dxcallsign = received_json["dxcallsign"]
data_handler.DATA_QUEUE_TRANSMIT.put(['PING', dxcallsign])
if received_json["type"] == 'ARQ' and received_json["command"] == "sendFile":
static.TNC_STATE = 'BUSY'
# on a new transmission we reset the timer
static.ARQ_START_OF_TRANSMISSION = int(time.time())
dxcallsign = received_json["dxcallsign"]
mode = int(received_json["mode"])
n_frames = int(received_json["n_frames"])
filename = received_json["filename"]
filetype = received_json["filetype"]
data = received_json["data"]
checksum = received_json["checksum"]
static.DXCALLSIGN = bytes(dxcallsign, 'utf-8')
static.DXCALLSIGN_CRC8 = helpers.get_crc_8(static.DXCALLSIGN)
def send_to_client(self):
"""
function called by socket handler
send data to a network client if available
# dt = datatype
# --> f = file
# --> m = message
# fn = filename
# ft = filetype
# d = data
# crc = checksum
rawdata = {"dt": "f", "fn": filename, "ft": filetype,"d": data, "crc": checksum}
dataframe = json.dumps(rawdata)
data_out = bytes(dataframe, 'utf-8')
data_handler.DATA_QUEUE_TRANSMIT.put(['ARQ_FILE', data_out, mode, n_frames])
"""
tempdata = b''
while self.connection_alive and not CLOSE_SIGNAL:
# send message
if received_json["type"] == 'ARQ' and received_json["command"] == "sendMessage":
static.TNC_STATE = 'BUSY'
print(received_json)
# on a new transmission we reset the timer
static.ARQ_START_OF_TRANSMISSION = int(time.time())
dxcallsign = received_json["dxcallsign"]
mode = int(received_json["mode"])
n_frames = int(received_json["n_frames"])
data = received_json["data"] # d = data
checksum = received_json["checksum"] # crc = checksum
static.DXCALLSIGN = bytes(dxcallsign, 'utf-8')
static.DXCALLSIGN_CRC8 = helpers.get_crc_8(static.DXCALLSIGN)
# dt = datatype
# --> f = file
# --> m = message
# fn = filename
# ft = filetype
# d = data
# crc = checksum
rawdata = {"dt": "m","d": data, "crc": checksum}
dataframe = json.dumps(rawdata)
data_out = bytes(dataframe, 'utf-8')
data_handler.DATA_QUEUE_TRANSMIT.put(['ARQ_MESSAGE', data_out, mode, n_frames])
if received_json["type"] == 'ARQ' and received_json["command"] == "stopTransmission":
data_handler.DATA_QUEUE_TRANSMIT.put(['STOP'])
print(" >>> STOPPING TRANSMISSION <<<")
structlog.get_logger("structlog").warning("[TNC] Stopping transmission!")
static.TNC_STATE = 'IDLE'
static.ARQ_STATE = False
if received_json["type"] == 'GET' and received_json["command"] == 'STATION_INFO':
output = {
"COMMAND": "STATION_INFO",
"TIMESTAMP": received_json["timestamp"],
"MY_CALLSIGN": str(static.MYCALLSIGN, encoding),
"DX_CALLSIGN": str(static.DXCALLSIGN, encoding),
"DX_GRID": str(static.DXGRID, encoding),
"EOF": "EOF",
}
jsondata = json.dumps(output)
self.request.sendall(bytes(jsondata, encoding))
if received_json["type"] == 'GET' and received_json["command"] == 'TNC_STATE':
output = {
"COMMAND": "TNC_STATE",
"TIMESTAMP": received_json["timestamp"],
"PTT_STATE": str(static.PTT_STATE),
#"CHANNEL_STATE": str(static.CHANNEL_STATE),
"TNC_STATE": str(static.TNC_STATE),
"ARQ_STATE": str(static.ARQ_STATE),
"AUDIO_RMS": str(static.AUDIO_RMS),
"SNR": str(static.SNR),
"FREQUENCY": str(static.HAMLIB_FREQUENCY),
"MODE": str(static.HAMLIB_MODE),
"BANDWITH": str(static.HAMLIB_BANDWITH),
"FFT": str(static.FFT),
"SCATTER": static.SCATTER,
"RX_BUFFER_LENGTH": str(len(static.RX_BUFFER)),
"RX_MSG_BUFFER_LENGTH": str(len(static.RX_MSG_BUFFER)),
"ARQ_BYTES_PER_MINUTE": str(static.ARQ_BYTES_PER_MINUTE),
"ARQ_BYTES_PER_MINUTE_BURST": str(static.ARQ_BYTES_PER_MINUTE_BURST),
"ARQ_COMPRESSION_FACTOR": str(static.ARQ_COMPRESSION_FACTOR),
"ARQ_TRANSMISSION_PERCENT": str(static.ARQ_TRANSMISSION_PERCENT),
"TOTAL_BYTES": str(static.TOTAL_BYTES),
"INFO" : static.INFO,
"BEACON_STATE" : str(static.BEACON_STATE),
"STATIONS": [],
"EOF": "EOF",
}
# we want to transmit scatter data only once to reduce network traffic
static.SCATTER = []
# we want to display INFO messages only once
static.INFO = []
# add heard stations to heard stations object
for i in range(0, len(static.HEARD_STATIONS)):
output["STATIONS"].append({"DXCALLSIGN": str(static.HEARD_STATIONS[i][0], 'utf-8'), "DXGRID": str(static.HEARD_STATIONS[i][1], 'utf-8'),"TIMESTAMP": static.HEARD_STATIONS[i][2], "DATATYPE": static.HEARD_STATIONS[i][3], "SNR": static.HEARD_STATIONS[i][4], "OFFSET": static.HEARD_STATIONS[i][5], "FREQUENCY": static.HEARD_STATIONS[i][6]})
# send tnc state as network stream
# check server port against daemon port and send corresponding data
if self.server.server_address[1] == static.PORT and not static.TNCSTARTED:
data = send_tnc_state()
if data != tempdata:
tempdata = data
SOCKET_QUEUE.put(data)
else:
data = send_daemon_state()
if data != tempdata:
tempdata = data
SOCKET_QUEUE.put(data)
time.sleep(0.5)
while not SOCKET_QUEUE.empty():
data = SOCKET_QUEUE.get()
sock_data = bytes(data, 'utf-8')
sock_data += b'\n' # append line limiter
# send data to all clients
#try:
for client in CONNECTED_CLIENTS:
try:
jsondata = json.dumps(output)
except ValueError as e:
structlog.get_logger("structlog").error(e, data=jsondata)
try:
self.request.sendall(bytes(jsondata, encoding))
client.send(sock_data)
except Exception as e:
structlog.get_logger("structlog").error(e, data=jsondata)
print("connection lost...")
print(e)
self.connection_alive = False
if received_json["type"] == 'GET' and received_json["command"] == 'RX_BUFFER':
output = {
"COMMAND": "RX_BUFFER",
"DATA-ARRAY": [],
"EOF": "EOF",
}
for i in range(0, len(static.RX_BUFFER)):
# 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)
rawdata = json.loads(static.RX_BUFFER[i][3])
output["DATA-ARRAY"].append({"DXCALLSIGN": str(static.RX_BUFFER[i][0], 'utf-8'), "DXGRID": str(static.RX_BUFFER[i][1], 'utf-8'), "TIMESTAMP": static.RX_BUFFER[i][2], "RXDATA": [rawdata]})
jsondata = json.dumps(output)
self.request.sendall(bytes(jsondata, encoding))
if received_json["type"] == 'GET' and received_json["command"] == 'RX_MSG_BUFFER':
output = {
"COMMAND": "RX_MSG_BUFFER",
"DATA-ARRAY": [],
"EOF": "EOF",
}
for i in range(0, len(static.RX_MSG_BUFFER)):
rawdata = json.loads(static.RX_MSG_BUFFER[i][3])
output["DATA-ARRAY"].append({"DXCALLSIGN": str(static.RX_MSG_BUFFER[i][0], 'utf-8'), "DXGRID": str(static.RX_MSG_BUFFER[i][1], 'utf-8'), "TIMESTAMP": static.RX_MSG_BUFFER[i][2], "RXDATA": [rawdata]})
jsondata = json.dumps(output)
self.request.sendall(bytes(jsondata, encoding))
def receive_from_client(self):
"""
function which is called by the socket handler
it processes the data which is returned by a client
"""
data = bytes()
while self.connection_alive and not CLOSE_SIGNAL:
try:
chunk = self.request.recv(1024)
data += chunk
print(data)
if chunk == b'':
#print("connection broken. Closing...")
self.connection_alive = False
if received_json["type"] == 'SET' and received_json["command"] == 'DEL_RX_BUFFER':
static.RX_BUFFER = []
if received_json["type"] == 'SET' and received_json["command"] == 'DEL_RX_MSG_BUFFER':
static.RX_MSG_BUFFER = []
# exception, if JSON cant be decoded
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 thorugh data list
for commands in data:
if self.server.server_address[1] == static.PORT:
process_tnc_commands(commands)
else:
process_daemon_commands(commands)
# wait some time between processing multiple commands
# this is only a first test to avoid doubled transmission
# 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)
# finally delete our rx buffer to be ready for new commands
data = bytes()
except Exception as e:
#socketTimeout = 0
structlog.get_logger("structlog").error("[TNC] Network error", e=e)
structlog.get_logger("structlog").warning("[TNC] Closing client socket", ip=self.client_address[0], port=self.client_address[1])
structlog.get_logger("structlog").info("[SCK] Connection closed", ip=self.client_address[0], port=self.client_address[1], e=e)
self.connection_alive = False
def handle(self):
"""
socket handler
"""
CONNECTED_CLIENTS.add(self.request)
structlog.get_logger("structlog").debug("[SCK] Client connected", ip=self.client_address[0], port=self.client_address[1])
self.connection_alive = True
self.sendThread = threading.Thread(target=self.send_to_client, args=[],daemon=True).start()
self.receiveThread = threading.Thread(target=self.receive_from_client, args=[],daemon=True).start()
# keep connection alive until we close it
while self.connection_alive and not CLOSE_SIGNAL:
time.sleep(1)
def finish(self):
""" """
structlog.get_logger("structlog").warning("[SCK] Closing client socket", ip=self.client_address[0], port=self.client_address[1])
try:
CONNECTED_CLIENTS.remove(self.request)
except:
print("client connection already removed from client list")
def process_tnc_commands(data):
"""
process tnc commands
Args:
data:
Returns:
"""
# we need to do some error handling in case of socket timeout or decoding issue
try:
# convert data to json object
received_json = json.loads(data)
# CQ CQ CQ -----------------------------------------------------
if received_json["command"] == "cqcqcq":
try:
data_handler.DATA_QUEUE_TRANSMIT.put(['CQ'])
command_response("cqcqcq", True)
except Exception as e:
command_response("cqcqcq", False)
structlog.get_logger("structlog").warning("[SCK] command execution error", e=e, command=received_json)
# START_BEACON -----------------------------------------------------
if received_json["command"] == "start_beacon":
try:
static.BEACON_STATE = True
interval = int(received_json["parameter"])
data_handler.DATA_QUEUE_TRANSMIT.put(['BEACON', interval, True])
command_response("start_beacon", True)
except Exception as e:
command_response("start_beacon", False)
structlog.get_logger("structlog").warning("[SCK] command execution error", e=e, command=received_json)
# STOP_BEACON -----------------------------------------------------
if received_json["command"] == "stop_beacon":
try:
structlog.get_logger("structlog").warning("[TNC] Stopping beacon!")
static.BEACON_STATE = False
data_handler.DATA_QUEUE_TRANSMIT.put(['BEACON', None, False])
command_response("stop_beacon", True)
except Exception as e:
command_response("stop_beacon", False)
structlog.get_logger("structlog").warning("[SCK] command execution error", e=e, command=received_json)
# PING ----------------------------------------------------------
if received_json["type"] == 'ping' and received_json["command"] == "ping":
# send ping frame and wait for ACK
try:
dxcallsign = received_json["dxcallsign"]
# 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)
data_handler.DATA_QUEUE_TRANSMIT.put(['PING', dxcallsign])
command_response("ping", True)
except Exception as e:
command_response("ping", False)
structlog.get_logger("structlog").warning("[SCK] command execution error", e=e, command=received_json)
# CONNECT ----------------------------------------------------------
if received_json["type"] == 'arq' and received_json["command"] == "connect":
static.BEACON_PAUSE = True
# send ping frame and wait for ACK
try:
dxcallsign = received_json["dxcallsign"]
# 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)
static.DXCALLSIGN = dxcallsign
static.DXCALLSIGN_CRC = helpers.get_crc_16(static.DXCALLSIGN)
data_handler.DATA_QUEUE_TRANSMIT.put(['CONNECT', dxcallsign])
command_response("connect", True)
except Exception as e:
command_response("connect", False)
structlog.get_logger("structlog").warning("[SCK] command execution error", e=e, command=received_json)
# DISCONNECT ----------------------------------------------------------
if received_json["type"] == 'arq' and received_json["command"] == "disconnect":
# send ping frame and wait for ACK
try:
data_handler.DATA_QUEUE_TRANSMIT.put(['DISCONNECT'])
command_response("disconnect", True)
except Exception as e:
command_response("disconnect", False)
structlog.get_logger("structlog").warning("[SCK] command execution error", e=e, command=received_json)
# TRANSMIT RAW DATA -------------------------------------------
if received_json["type"] == 'arq' and received_json["command"] == "send_raw":
print(received_json)
static.BEACON_PAUSE = True
try:
if not static.ARQ_SESSION:
dxcallsign = received_json["parameter"][0]["dxcallsign"]
# 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)
static.DXCALLSIGN = dxcallsign
static.DXCALLSIGN_CRC = helpers.get_crc_16(static.DXCALLSIGN)
command_response("send_raw", True)
else:
dxcallsign = static.DXCALLSIGN
static.DXCALLSIGN_CRC = helpers.get_crc_16(static.DXCALLSIGN)
mode = int(received_json["parameter"][0]["mode"])
n_frames = int(received_json["parameter"][0]["n_frames"])
base64data = received_json["parameter"][0]["data"]
if not len(base64data) % 4:
binarydata = base64.b64decode(base64data)
data_handler.DATA_QUEUE_TRANSMIT.put(['ARQ_RAW', binarydata, mode, n_frames])
else:
raise TypeError
except Exception as e:
command_response("send_raw", False)
structlog.get_logger("structlog").warning("[SCK] command execution error", e=e, command=received_json)
# STOP TRANSMISSION ----------------------------------------------------------
if received_json["type"] == 'arq' and received_json["command"] == "stop_transmission":
try:
if static.TNC_STATE == 'BUSY' or static.ARQ_STATE:
data_handler.DATA_QUEUE_TRANSMIT.put(['STOP'])
structlog.get_logger("structlog").warning("[TNC] Stopping transmission!")
static.TNC_STATE = 'IDLE'
static.ARQ_STATE = False
command_response("stop_transmission", True)
except Exception as e:
command_response("stop_transmission", False)
structlog.get_logger("structlog").warning("[SCK] command execution error", e=e, command=received_json)
if received_json["type"] == 'get' and received_json["command"] == 'rx_buffer':
try:
output = {
"command": "rx_buffer",
"data-array": [],
}
for i in range(0, len(static.RX_BUFFER)):
#print(static.RX_BUFFER[i][4])
#rawdata = json.loads(static.RX_BUFFER[i][4])
base64_data = static.RX_BUFFER[i][4]
output["data-array"].append({"uuid": static.RX_BUFFER[i][0],"timestamp": static.RX_BUFFER[i][1], "dxcallsign": str(static.RX_BUFFER[i][2], 'utf-8'), "dxgrid": str(static.RX_BUFFER[i][3], 'utf-8'), "data": base64_data})
jsondata = json.dumps(output)
#self.request.sendall(bytes(jsondata, encoding))
SOCKET_QUEUE.put(jsondata)
command_response("rx_buffer", True)
except Exception as e:
command_response("rx_buffer", False)
structlog.get_logger("structlog").warning("[SCK] command execution error", e=e, command=received_json)
if received_json["type"] == 'set' and received_json["command"] == 'del_rx_buffer':
try:
static.RX_BUFFER = []
command_response("del_rx_buffer", True)
except Exception as e:
command_response("del_rx_buffer", False)
structlog.get_logger("structlog").warning("[SCK] command execution error", e=e, command=received_json)
# exception, if JSON cant be decoded
except Exception as e:
structlog.get_logger("structlog").error("[TNC] JSON decoding error", e=e)
def send_tnc_state():
"""
send the tnc state to network
"""
encoding = 'utf-8'
output = {
"command": "tnc_state",
"ptt_state": str(static.PTT_STATE),
"tnc_state": str(static.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),
"snr": str(static.SNR),
"frequency": str(static.HAMLIB_FREQUENCY),
"speed_level": str(static.ARQ_SPEED_LEVEL),
"mode": str(static.HAMLIB_MODE),
"bandwith": str(static.HAMLIB_BANDWITH),
"fft": str(static.FFT),
"channel_busy": str(static.CHANNEL_BUSY),
"scatter": static.SCATTER,
"rx_buffer_length": str(len(static.RX_BUFFER)),
"rx_msg_buffer_length": str(len(static.RX_MSG_BUFFER)),
"arq_bytes_per_minute": str(static.ARQ_BYTES_PER_MINUTE),
"arq_bytes_per_minute_burst": str(static.ARQ_BYTES_PER_MINUTE_BURST),
"arq_compression_factor": str(static.ARQ_COMPRESSION_FACTOR),
"arq_transmission_percent": str(static.ARQ_TRANSMISSION_PERCENT),
"total_bytes": str(static.TOTAL_BYTES),
"info" : static.INFO,
"beacon_state" : str(static.BEACON_STATE),
"stations": [],
"mycallsign": str(static.MYCALLSIGN, encoding),
"dxcallsign": str(static.DXCALLSIGN, encoding),
"dxgrid": str(static.DXGRID, encoding),
}
# add heard stations to heard stations object
for i in range(0, len(static.HEARD_STATIONS)):
output["stations"].append({"dxcallsign": str(static.HEARD_STATIONS[i][0], 'utf-8'), "dxgrid": str(static.HEARD_STATIONS[i][1], 'utf-8'),"timestamp": static.HEARD_STATIONS[i][2], "datatype": static.HEARD_STATIONS[i][3], "snr": static.HEARD_STATIONS[i][4], "offset": static.HEARD_STATIONS[i][5], "frequency": static.HEARD_STATIONS[i][6]})
jsondata = json.dumps(output)
return jsondata
def process_daemon_commands(data):
"""
process daemon commands
Args:
data:
Returns:
"""
# convert data to json object
received_json = json.loads(data)
if received_json["type"] == 'set' and received_json["command"] == 'mycallsign':
try:
callsign = received_json["parameter"]
if bytes(callsign, 'utf-8') == b'':
self.request.sendall(b'INVALID CALLSIGN')
structlog.get_logger("structlog").warning("[DMN] SET MYCALL FAILED", call=static.MYCALLSIGN, crc=static.MYCALLSIGN_CRC)
else:
static.MYCALLSIGN = bytes(callsign, 'utf-8')
static.MYCALLSIGN_CRC = helpers.get_crc_16(static.MYCALLSIGN)
command_response("mycallsign", True)
structlog.get_logger("structlog").info("[DMN] SET MYCALL", call=static.MYCALLSIGN, crc=static.MYCALLSIGN_CRC)
except Exception as e:
command_response("mycallsign", False)
structlog.get_logger("structlog").warning("[SCK] command execution error", e=e, command=received_json)
if received_json["type"] == 'set' and received_json["command"] == 'mygrid':
try:
mygrid = received_json["parameter"]
if bytes(mygrid, 'utf-8') == b'':
self.request.sendall(b'INVALID GRID')
else:
static.MYGRID = bytes(mygrid, 'utf-8')
structlog.get_logger("structlog").info("[SCK] SET MYGRID", grid=static.MYGRID)
command_response("mygrid", True)
except Exception as e:
command_response("mygrid", False)
structlog.get_logger("structlog").warning("[SCK] command execution error", e=e, command=received_json)
if received_json["type"] == 'set' and received_json["command"] == 'start_tnc' and not static.TNCSTARTED:
try:
mycall = str(received_json["parameter"][0]["mycall"])
mygrid = str(received_json["parameter"][0]["mygrid"])
rx_audio = str(received_json["parameter"][0]["rx_audio"])
tx_audio = str(received_json["parameter"][0]["tx_audio"])
devicename = str(received_json["parameter"][0]["devicename"])
deviceport = str(received_json["parameter"][0]["deviceport"])
serialspeed = str(received_json["parameter"][0]["serialspeed"])
pttprotocol = str(received_json["parameter"][0]["pttprotocol"])
pttport = str(received_json["parameter"][0]["pttport"])
data_bits = str(received_json["parameter"][0]["data_bits"])
stop_bits = str(received_json["parameter"][0]["stop_bits"])
handshake = str(received_json["parameter"][0]["handshake"])
radiocontrol = str(received_json["parameter"][0]["radiocontrol"])
rigctld_ip = str(received_json["parameter"][0]["rigctld_ip"])
rigctld_port = str(received_json["parameter"][0]["rigctld_port"])
enable_scatter = str(received_json["parameter"][0]["enable_scatter"])
enable_fft = str(received_json["parameter"][0]["enable_fft"])
low_bandwith_mode = str(received_json["parameter"][0]["low_bandwith_mode"])
DAEMON_QUEUE.put(['STARTTNC', \
mycall, \
mygrid, \
rx_audio, \
tx_audio, \
devicename, \
deviceport, \
serialspeed, \
pttprotocol, \
pttport, \
data_bits, \
stop_bits, \
handshake, \
radiocontrol, \
rigctld_ip, \
rigctld_port, \
enable_scatter, \
enable_fft, \
low_bandwith_mode \
])
command_response("start_tnc", True)
except Exception as e:
command_response("start_tnc", False)
structlog.get_logger("structlog").warning("[SCK] command execution error", e=e, command=received_json)
if received_json["type"] == 'get' and received_json["command"] == 'test_hamlib':
try:
devicename = str(received_json["parameter"][0]["devicename"])
deviceport = str(received_json["parameter"][0]["deviceport"])
serialspeed = str(received_json["parameter"][0]["serialspeed"])
pttprotocol = str(received_json["parameter"][0]["pttprotocol"])
pttport = str(received_json["parameter"][0]["pttport"])
data_bits = str(received_json["parameter"][0]["data_bits"])
stop_bits = str(received_json["parameter"][0]["stop_bits"])
handshake = str(received_json["parameter"][0]["handshake"])
radiocontrol = str(received_json["parameter"][0]["radiocontrol"])
rigctld_ip = str(received_json["parameter"][0]["rigctld_ip"])
rigctld_port = str(received_json["parameter"][0]["rigctld_port"])
DAEMON_QUEUE.put(['TEST_HAMLIB', \
devicename, \
deviceport, \
serialspeed, \
pttprotocol, \
pttport, \
data_bits, \
stop_bits, \
handshake, \
radiocontrol, \
rigctld_ip, \
rigctld_port \
])
command_response("test_hamlib", True)
except Exception as e:
command_response("test_hamlib", False)
structlog.get_logger("structlog").warning("[SCK] command execution error", e=e, command=received_json)
if received_json["type"] == 'set' and received_json["command"] == 'stop_tnc':
try:
static.TNCPROCESS.kill()
# unregister process from atexit to avoid process zombies
atexit.unregister(static.TNCPROCESS.kill)
structlog.get_logger("structlog").warning("[DMN] Stopping TNC")
static.TNCSTARTED = False
command_response("stop_tnc", True)
except Exception as e:
command_response("stop_tnc", False)
structlog.get_logger("structlog").warning("[SCK] command execution error", e=e, command=received_json)
def send_daemon_state():
"""
send the daemon state to network
"""
try:
python_version = str(sys.version_info[0]) + "." + str(sys.version_info[1])
output = {
'command': 'daemon_state',
'daemon_state': [],
'python_version': str(python_version),
'hamlib_version': static.HAMLIB_VERSION,
'input_devices': static.AUDIO_INPUT_DEVICES,
'output_devices': static.AUDIO_OUTPUT_DEVICES,
'serial_devices': static.SERIAL_DEVICES,
#'cpu': str(psutil.cpu_percent()),
#'ram': str(psutil.virtual_memory().percent),
'version': '0.1'
}
if static.TNCSTARTED:
output["daemon_state"].append({"status": "running"})
else:
output["daemon_state"].append({"status": "stopped"})
jsondata = json.dumps(output)
return jsondata
except Exception as e:
print(e)
return None
def command_response(command, status):
if status:
status = "OK"
else:
status = "Failed"
jsondata = {"command_response": command, "status" : status}
data_out = json.dumps(jsondata)
SOCKET_QUEUE.put(data_out)

View file

@ -5,9 +5,11 @@ Created on Wed Dec 23 11:13:57 2020
@author: DJ2LS
Here we are saving application wide variables and stats, which have to be accessed everywhere.
Not nice, tipps are appreciated :-)
Not nice, suggestions are appreciated :-)
"""
VERSION = '0.1.0-alpha'
# DAEMON
DAEMONPORT = 3001
TNCSTARTED = False
@ -16,14 +18,15 @@ TNCPROCESS = 0
# Operator Defaults
MYCALLSIGN = b'AA0AA'
MYCALLSIGN_CRC8 = b'A'
MYCALLSIGN_CRC = b'A'
DXCALLSIGN = b'AA0AA'
DXCALLSIGN_CRC8 = b'A'
DXCALLSIGN_CRC = b'A'
MYGRID = b''
DXGRID = b''
LOW_BANDWITH_MODE = False
# ---------------------------------
# Server Defaults
@ -31,12 +34,14 @@ HOST = "0.0.0.0"
PORT = 3000
SOCKET_TIMEOUT = 1 # seconds
# ---------------------------------
SERIAL_DEVICES = []
# ---------------------------------
PTT_STATE = False
TRANSMITTING = False
HAMLIB_VERSION = '0'
HAMLIB_PTT_TYPE = 'RTS'
HAMLIB_DEVICE_NAME = 'RIG_MODEL_DUMMY_NOVFO'
HAMLIB_DEVICE_PORT = '/dev/ttyUSB0'
@ -46,8 +51,8 @@ HAMLIB_STOP_BITS = '1'
HAMLIB_DATA_BITS = '8'
HAMLIB_HANDSHAKE = 'None'
HAMLIB_RADIOCONTROL = 'direct'
HAMLIB_RGICTLD_IP = '127.0.0.1'
HAMLIB_RGICTLD_PORT = '4532'
HAMLIB_RIGCTLD_IP = '127.0.0.1'
HAMLIB_RIGCTLD_PORT = '4532'
HAMLIB_FREQUENCY = 0
HAMLIB_MODE = ''
@ -58,16 +63,23 @@ HAMLIB_BANDWITH = 0
SNR = 0
FREQ_OFFSET = 0
SCATTER = []
ENABLE_SCATTER = False
# ---------------------------------
# Audio Defaults
AUDIO_INPUT_DEVICES = []
AUDIO_OUTPUT_DEVICES = []
AUDIO_INPUT_DEVICE = -2
AUDIO_OUTPUT_DEVICE = -2
BUFFER_OVERFLOW_COUNTER = [0,0,0]
AUDIO_RMS = 0
FFT = []
FFT = [0]
ENABLE_FFT = False
CHANNEL_BUSY = None
# ARQ PROTOCOL VERSION
ARQ_PROTOCOL_VERSION = 0
# ARQ statistics
ARQ_BYTES_PER_MINUTE_BURST = 0
@ -76,15 +88,19 @@ ARQ_BITS_PER_SECOND_BURST = 0
ARQ_BITS_PER_SECOND = 0
ARQ_COMPRESSION_FACTOR = 0
ARQ_TRANSMISSION_PERCENT = 0
ARQ_SPEED_LEVEL = 0
TOTAL_BYTES = 0
#CHANNEL_STATE = 'RECEIVING_SIGNALLING'
TNC_STATE = 'IDLE'
ARQ_STATE = False
ARQ_SESSION = False
ARQ_SESSION_STATE = 'disconnected' # disconnected, connecting, connected, disconnecting, failed
# BEACON STATE
BEACON_STATE = False
BEACON_PAUSE = False
# ------- RX BUFFER
RX_BUFFER = []