Merge pull request #473 from DJ2LS/ls-gui-single-pager

GUI sidebar & GUI single pager & VueJS + Vite + Typescript
This commit is contained in:
DJ2LS 2023-10-23 00:22:52 +02:00 committed by GitHub
commit 4f2671c359
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
158 changed files with 14492 additions and 10622 deletions

View file

@ -13,7 +13,14 @@ updates:
directory: "/gui"
schedule:
interval: "daily"
# Maintain dependencies for npm
- package-ecosystem: "npm"
directory: "/gui"
schedule:
interval: "daily"
target-branch: "ls-gui-single-pager"
# Maintain dependencies for pip
- package-ecosystem: "pip"
directory: "/"

View file

@ -8,7 +8,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, ubuntu-22.04, macos-11, macos-12]
os: [ubuntu-20.04, ubuntu-22.04, macos-latest, macos-12]
platform: [{name: "native"}, {name: "Windows", file: "dll"}]
architecture: [i686-w64-mingw32, x86_64-w64-mingw32]
include:
@ -27,9 +27,9 @@ jobs:
generator: Unix Makefiles
shell: bash
- os: macos-11
- os: macos-latest
libcodec2_name: libcodec2.1.2.dylib
libcodec2_os_name: libcodec2_macos-11
libcodec2_os_name: libcodec2_macos-latest
libcodec2_filetype: dylib
generator: Unix Makefiles
shell: bash
@ -194,27 +194,27 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, macos-11, windows-latest]
os: [ubuntu-20.04, macos-latest, windows-latest]
include:
- os: ubuntu-20.04
zip_name: ubuntu_tnc
zip_name: ubuntu_modem
generator: Unix Makefiles
daemon_binary_name: freedata-daemon
tnc_binary_name: freedata-tnc
modem_binary_name: freedata-modem
electron_parameters: "-p always"
- os: macos-11
zip_name: macos_tnc
- os: macos-latest
zip_name: macos_modem
generator: Unix Makefiles
daemon_binary_name: freedata-daemon
tnc_binary_name: freedata-tnc
modem_binary_name: freedata-modem
electron_parameters: "-p always"
- os: windows-latest
zip_name: windows_tnc
zip_name: windows_modem
generator: Visual Studio 16 2019
daemon_binary_name: freedata-daemon.exe
tnc_binary_name: freedata-tnc.exe
modem_binary_name: freedata-modem.exe
electron_parameters: "-p always --x64 --ia32"
steps:
- name: Checkout code for ${{ matrix.platform.name }}
@ -232,30 +232,30 @@ jobs:
with:
node-version: 18.17
- name: Create tnc/dist
working-directory: tnc
- name: Create modem/dist
working-directory: modem
run: |
mkdir -p dist
- name: Create tnc/dist/tnc
working-directory: tnc
- name: Create modem/dist/modem
working-directory: modem
run: |
mkdir -p dist/tnc
mkdir -p dist/modem
##- name: Download libcodec2 artifact TNC DIST
##- name: Download libcodec2 artifact Modem DIST
## uses: actions/download-artifact@v3
## with:
## path: tnc/dist/codec2
## path: modem/dist/codec2
- name: create tnc/lib/codec2
working-directory: tnc/lib/
- name: create modem/lib/codec2
working-directory: modem/lib/
run: |
mkdir codec2
- name: Download libcodec2 artifact TNC LIB
- name: Download libcodec2 artifact Modem LIB
uses: actions/download-artifact@v3
with:
path: tnc/lib/codec2
path: modem/lib/codec2
- name: Install Linux dependencies
@ -285,50 +285,50 @@ jobs:
- name: Build binaries macOS
if: ${{startsWith(matrix.os, 'macos')}}
working-directory: tnc
working-directory: modem
run: |
# now build tnc binaries
# now build modem binaries
pyinstaller -y freedata.spec
# and to some final cleanup
# cp -r -f dist/tnc/* dist/
# rm -r dist/tnc
# cp -r -f dist/modem/* dist/
# rm -r dist/modem
- name: Build binaries Linux and Windows
if: ${{!startsWith(matrix.os, 'macos')}}
working-directory: tnc
working-directory: modem
run: |
# pyinstaller freedata.spec
# python3 -m nuitka --enable-plugin=numpy --assume-yes-for-downloads --onefile daemon.py -o ${{ matrix.daemon_binary_name }}
# python3 -m nuitka --enable-plugin=numpy --assume-yes-for-downloads --onefile main.py -o ${{ matrix.tnc_binary_name }}
# python3 -m nuitka --enable-plugin=numpy --assume-yes-for-downloads --onefile main.py -o ${{ matrix.modem_binary_name }}
python3 -m nuitka --enable-plugin=numpy --assume-yes-for-downloads --standalone daemon.py
python3 -m nuitka --enable-plugin=numpy --assume-yes-for-downloads --standalone main.py
- name: Copy binaries - Linux
if: ${{startsWith(matrix.os, 'ubuntu')}}
working-directory: tnc
working-directory: modem
run: |
cp -r -f daemon.dist/* dist/tnc
cp -r -f main.dist/* dist/tnc
cp -r -f daemon.dist/* dist/modem
cp -r -f main.dist/* dist/modem
- name: Copy binaries - Windows
if: ${{startsWith(matrix.os, 'windows')}}
working-directory: tnc
working-directory: modem
# These are powershell aliases, not UNIX commands.
run: |
cp -r -Force daemon.dist/* dist/tnc
cp -r -Force main.dist/* dist/tnc
cp -r -Force daemon.dist/* dist/modem
cp -r -Force main.dist/* dist/modem
- name: Rename tnc binaries
- name: Rename modem binaries
# we don't need renaming for pyinstaller builds as output name is defined
if: ${{!startsWith(matrix.os, 'macos')}}
working-directory: tnc
working-directory: modem
run: |
mv dist/tnc/daemon* dist/tnc/${{ matrix.daemon_binary_name }}
mv dist/tnc/main* dist/tnc/${{ matrix.tnc_binary_name }}
mv dist/modem/daemon* dist/modem/${{ matrix.daemon_binary_name }}
mv dist/modem/main* dist/modem/${{ matrix.modem_binary_name }}
- uses: actions/download-artifact@v3
with:
path: tnc/dist/tnc
path: modem/dist/modem
- name: LIST ALL FILES
@ -336,20 +336,20 @@ jobs:
- name: Download Portaudio binaries Linux macOS
if: ${{!startsWith(matrix.os, 'windows')}}
working-directory: tnc
working-directory: modem
run: |
if ! test -d "dist/tnc/_sounddevice_data"; then
git clone https://github.com/spatialaudio/portaudio-binaries dist/tnc/_sounddevice_data/portaudio-binaries
if ! test -d "dist/modem/_sounddevice_data"; then
git clone https://github.com/spatialaudio/portaudio-binaries dist/modem/_sounddevice_data/portaudio-binaries
fi
- name: Download Portaudio binaries Windows
if: ${{startsWith(matrix.os, 'windows')}}
working-directory: tnc
working-directory: modem
run: |
if(Test-Path -Path "dist/tnc/_sounddevice_data"){
if(Test-Path -Path "dist/modem/_sounddevice_data"){
echo "sounddevice folder already exists"
} else {
git clone https://github.com/spatialaudio/portaudio-binaries dist/tnc/_sounddevice_data/portaudio-binaries
git clone https://github.com/spatialaudio/portaudio-binaries dist/modem/_sounddevice_data/portaudio-binaries
}
- name: LIST ALL FILES
@ -362,53 +362,50 @@ jobs:
# find . -type d -name .git -exec rm -r {} \;
find . -type d -o -name ".git" -delete
- name: Build/release Electron app
uses: coparse-inc/action-electron-builder@v1.0.0
env:
- name: Electron Builder
env: # Setting environment variables for the entire job
GH_TOKEN: ${{ secrets.github_token }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
with:
package_root: "./gui/"
github_token: ${{ secrets.github_token }}
# If the commit is tagged with a version (e.g. "v1.0.0"),
# release the app after building
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
args: ${{ matrix.electron_parameters }}
max_attempts: 3
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
working-directory: gui
run: |
npm i
npm run build
- name: Compress TNC
- name: Compress Modem
uses: thedoctor0/zip-release@master
with:
type: 'zip'
filename: '${{ matrix.zip_name }}.zip'
# directory: ./tnc/dist/tnc
directory: ./tnc/dist/tnc
# directory: ./modem/dist/modem
directory: ./modem/dist/modem
path: .
# exclusions: '*.git* /*node_modules/* .editorconfig'
- name: Release TNC
- name: Release Modem
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/v')
with:
files: ./tnc/dist/tnc/${{ matrix.zip_name }}.zip
#files: ./tnc/dist/${{ matrix.zip_name }}.zip
files: ./modem/dist/modem/${{ matrix.zip_name }}.zip
#files: ./modem/dist/${{ matrix.zip_name }}.zip
- name: LIST ALL FILES
run: ls -R
#- name: Upload TNC artifacts
#- name: Upload Modem artifacts
# uses: actions/upload-artifact@v3
# if: ${{!startsWith(github.ref, 'refs/tags/v')}}
# with:
# name: ${{ matrix.zip_name }}.zip
# # path: ./tnc/dist/tnc/${{ matrix.zip_name }}.zip
# path: ./tnc/dist/tnc/${{ matrix.zip_name }}.zip#
# # path: ./modem/dist/modem/${{ matrix.zip_name }}.zip
# path: ./modem/dist/modem/${{ matrix.zip_name }}.zip#
#- name: Upload App bundle artifacts
# uses: actions/upload-artifact@v3
# if: ${{!startsWith(github.ref, 'refs/tags/v')}}
# with:
# name: app_bundle_${{ matrix.os }}.zip
# # path: ./tnc/dist/tnc/${{ matrix.zip_name }}.zip
# # path: ./modem/dist/modem/${{ matrix.zip_name }}.zip
# path: ./gui/dist/*

12
.gitignore vendored
View file

@ -1,5 +1,5 @@
# possible installation of codec2 within tnc
tnc/codec2
# possible installation of codec2 within modem
modem/codec2
# temporary test artifacts
**/build
@ -22,3 +22,11 @@ package-lock.json
*.raw
coverage.sh
coverage.xml
#ignore node_modules
/gui/node_modules/
#Ignore gui build items
/gui/dist
/gui/release
/gui/dist-electron

View file

@ -24,43 +24,43 @@ set(TESTFRAMES 3)
add_test(NAME audio_buffer
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
export PYTHONPATH=../modem;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_audiobuffer.py")
set_tests_properties(audio_buffer PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
add_test(NAME resampler
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
export PYTHONPATH=../modem;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_resample_48_8.py")
set_tests_properties(resampler PROPERTIES PASS_REGULAR_EXPRESSION "PASS")
add_test(NAME tnc_state_machine
add_test(NAME modem_state_machine
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
export PYTHONPATH=../modem;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_tnc_states.py")
set_tests_properties(tnc_state_machine PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
python3 test_modem_states.py")
set_tests_properties(modem_state_machine PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
add_test(NAME tnc_irs_iss
add_test(NAME modem_irs_iss
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
export PYTHONPATH=../modem;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_tnc.py")
set_tests_properties(tnc_irs_iss PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
python3 test_modem.py")
set_tests_properties(modem_irs_iss PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
# disabled this test as its actually broken since we introduced session IDs
#add_test(NAME chat_text
# COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
# export PYTHONPATH=../tnc;
# export PYTHONPATH=../modem;
# cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
# python3 test_chat_text.py")
# set_tests_properties(chat_text PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
add_test(NAME datac13_frames
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
export PYTHONPATH=../modem;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_datac13.py")
set_tests_properties(datac13_frames PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
@ -68,21 +68,21 @@ add_test(NAME datac13_frames
# disabled this test as its actually broken since we introduced dataclasses
#add_test(NAME datac13_frames_negative
# COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
# export PYTHONPATH=../tnc;
# export PYTHONPATH=../modem;
# cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
# python3 test_datac13_negative.py")
# set_tests_properties(datac13_frames_negative PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
add_test(NAME helper_routines
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
export PYTHONPATH=../modem;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_helpers.py")
set_tests_properties(helper_routines PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
add_test(NAME py_highsnr_stdio_P_P_multi
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
export PYTHONPATH=../modem;
export BURSTS=${BURSTS};
export FRAMESPERBURST=${FRAMESPERBURST};
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
@ -92,7 +92,7 @@ add_test(NAME py_highsnr_stdio_P_P_multi
add_test(NAME py_highsnr_stdio_P_P_datacx
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
export PYTHONPATH=../modem;
export BURSTS=${BURSTS};
export FRAMESPERBURST=${FRAMESPERBURST};
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
@ -102,7 +102,7 @@ add_test(NAME py_highsnr_stdio_P_P_datacx
add_test(NAME py_highsnr_stdio_P_C_datacx
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
export PYTHONPATH=../modem;
export BURSTS=${BURSTS};
export FRAMESPERBURST=${FRAMESPERBURST};
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
@ -112,7 +112,7 @@ add_test(NAME py_highsnr_stdio_P_C_datacx
add_test(NAME py_highsnr_stdio_C_P_datacx
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
export PYTHONPATH=../modem;
export BURSTS=${BURSTS};
export FRAMESPERBURST=${FRAMESPERBURST};
export TESTFRAMES=${TESTFRAMES};

23
gui/.eslintrc.json Normal file
View file

@ -0,0 +1,23 @@
{
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"plugin:prettier/recommended",
"eslint:recommended"
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": [
"vue"
],
"ignorePatterns": ["**/src/assets/*", "**/src/js/deprecated*", "**/node_modules"],
"rules": {
}
}

104
gui/.gitignore vendored
View file

@ -1,104 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port

18
gui/README.md Normal file
View file

@ -0,0 +1,18 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support For `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.

View file

@ -0,0 +1,23 @@
const { notarize } = require('@electron/notarize');
async function notarizing(context) {
const { electronPlatformName, appOutDir } = context;
console.log("platform:" + electronPlatformName)
if (electronPlatformName !== 'darwin') {
console.log("not a APPLE system")
return;
}
const appName = context.packager.appInfo.productFilename;
return await notarize({
tool: 'notarytool',
appBundleId: 'app.freedata',
appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_ID_PASSWORD,
teamId: process.env.APPLE_TEAM_ID
});
}
exports.default = notarizing;

View file

@ -1,351 +0,0 @@
var net = require("net");
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");
var configFolder = path.join(appDataFolder, "FreeDATA");
var configPath = path.join(configFolder, "config.json");
const config = require(configPath);
var daemon = new net.Socket();
var socketchunk = ""; // Current message, per connection.
// global to keep track of daemon connection error emissions
var daemonShowConnectStateError = 1;
// global for storing ip information
var daemon_port = config.daemon_port;
var daemon_host = config.daemon_host;
setTimeout(connectDAEMON, 500);
function connectDAEMON() {
if (daemonShowConnectStateError == 1) {
daemonLog.info("connecting to daemon");
}
//clear message buffer after reconnecting or initial connection
socketchunk = "";
if (config.tnclocation == "localhost") {
daemon.connect(3001, "127.0.0.1");
} else {
daemon.connect(daemon_port, daemon_host);
}
//client.setTimeout(5000);
}
daemon.on("connect", function (err) {
daemonLog.info("daemon connection established");
let Data = {
daemon_connection: daemon.readyState,
};
ipcRenderer.send("request-update-daemon-connection", Data);
daemonShowConnectStateError = 1;
});
daemon.on("error", function (err) {
if (daemonShowConnectStateError == 1) {
daemonLog.error("daemon connection error");
daemonLog.info("Make sure the daemon is started.");
daemonLog.info('Run "python daemon.py" in the tnc directory.');
daemonShowConnectStateError = 0;
}
setTimeout(connectDAEMON, 500);
daemon.destroy();
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) {
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){
writeDaemonCommand = function (command) {
// we use the writingCommand function to update our TCPIP state because we are calling this function a lot
// if socket opened, we are able to run commands
if (daemon.readyState == "open") {
//uiMain.setDAEMONconnection('open')
daemon.write(command + "\n");
}
if (daemon.readyState == "closed") {
//uiMain.setDAEMONconnection('closed')
}
if (daemon.readyState == "opening") {
//uiMain.setDAEMONconnection('opening')
}
let Data = {
daemon_connection: daemon.readyState,
};
ipcRenderer.send("request-update-daemon-connection", Data);
};
// "https://stackoverflow.com/questions/9070700/nodejs-net-createserver-large-amount-of-data-coming-in"
daemon.on("data", function (socketdata) {
/*
inspired by:
stackoverflow.com questions 9070700 nodejs-net-createserver-large-amount-of-data-coming-in
*/
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);
}
}
//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) {
for (var bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16));
return bytes;
}
exports.getDaemonState = function () {
//function getDaemonState(){
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,
enable_fft,
enable_scatter,
low_bandwidth_mode,
tuning_range_fmin,
tuning_range_fmax,
enable_fsk,
tx_audio_level,
respond_to_cq,
rx_buffer_size,
enable_explorer,
explorer_stats,
auto_tune,
tx_delay,
tci_ip,
tci_port,
enable_mesh,
) {
var json_command = JSON.stringify({
type: "set",
command: "start_tnc",
parameter: [
{
mycall: mycall,
mygrid: mygrid,
rx_audio: rx_audio,
tx_audio: tx_audio,
radiocontrol: radiocontrol,
devicename: devicename,
deviceport: deviceport,
pttprotocol: pttprotocol,
pttport: pttport,
serialspeed: serialspeed,
data_bits: data_bits,
stop_bits: stop_bits,
handshake: handshake,
rigctld_port: rigctld_port,
rigctld_ip: rigctld_ip,
enable_scatter: enable_scatter,
enable_fft: enable_fft,
enable_fsk: enable_fsk,
low_bandwidth_mode: low_bandwidth_mode,
tuning_range_fmin: tuning_range_fmin,
tuning_range_fmax: tuning_range_fmax,
tx_audio_level: tx_audio_level,
respond_to_cq: respond_to_cq,
rx_buffer_size: rx_buffer_size,
enable_explorer: enable_explorer,
enable_stats: explorer_stats,
enable_auto_tune: auto_tune,
tx_delay: tx_delay,
tci_ip: tci_ip,
tci_port: tci_port,
enable_mesh: enable_mesh,
},
],
});
daemonLog.debug(json_command);
writeDaemonCommand(json_command);
};
// STOP TNC
exports.stopTNC = function () {
command = '{"type" : "set", "command": "stop_tnc" , "parameter": "---" }';
writeDaemonCommand(command);
};
// TEST HAMLIB
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",
parameter: [
{
radiocontrol: radiocontrol,
devicename: devicename,
deviceport: deviceport,
pttprotocol: pttprotocol,
pttport: pttport,
serialspeed: serialspeed,
data_bits: data_bits,
stop_bits: stop_bits,
handshake: handshake,
rigctld_port: rigctld_port,
rigctld_ip: rigctld_ip,
},
],
});
daemonLog.debug(json_command);
writeDaemonCommand(json_command);
};
//Save myCall
exports.saveMyCall = function (callsign) {
command =
'{"type" : "set", "command": "mycallsign" , "parameter": "' +
callsign +
'"}';
writeDaemonCommand(command);
};
// Save myGrid
exports.saveMyGrid = function (grid) {
command =
'{"type" : "set", "command": "mygrid" , "parameter": "' + grid + '"}';
writeDaemonCommand(command);
};
ipcRenderer.on("action-update-daemon-ip", (event, arg) => {
daemon.destroy();
let Data = {
busy_state: "-",
arq_state: "-",
//channel_state: "-",
frequency: "-",
mode: "-",
bandwidth: "-",
dbfs_level: 0,
};
ipcRenderer.send("request-update-tnc-state", Data);
daemon_port = arg.port;
daemon_host = arg.adress;
connectDAEMON();
});

View file

@ -0,0 +1,67 @@
/**
* @see https://www.electron.build/configuration/configuration
*/
{
"$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json",
"appId": "app.freedata",
"asar": true,
"afterSign": "build/notarize_macos.js",
"productName": "FreeDATA",
"directories": {
"output": "release"
},
"files": [
"dist",
"dist-electron",
],
"extraResources": [
{
"from": "../modem/dist/modem/",
"to": "modem",
"filter": [
"**/*",
"!**/.git"
]
}
],
"mac": {
"target": [
"dmg"
],
"icon": "build/icon.png",
"hardenedRuntime": true,
"entitlements": "build/entitlements.plist",
"entitlementsInherit": "build/entitlements.plist",
"gatekeeperAssess": false,
"artifactName": "${productName}-Mac-${version}-Installer.${ext}"
},
"win": {
"icon": "build/icon.png",
"target": [
{
"target": "nsis",
"arch": [
"x64"
]
}
],
"artifactName": "${productName}-Windows-${version}-Setup.${ext}"
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true,
"deleteAppDataOnUninstall": true
},
"linux": {
"category": "Development",
"target": [
"AppImage"
],
"artifactName": "${productName}-${version}.${ext}"
}
}

11
gui/electron/electron-env.d.ts vendored Normal file
View file

@ -0,0 +1,11 @@
/// <reference types="vite-plugin-electron/electron-env" />
declare namespace NodeJS {
interface ProcessEnv {
VSCODE_DEBUG?: 'true'
DIST_ELECTRON: string
DIST: string
/** /dist/ or /public/ */
VITE_PUBLIC: string
}
}

289
gui/electron/main/index.ts Normal file
View file

@ -0,0 +1,289 @@
import { app, BrowserWindow, shell, ipcMain } from "electron";
import { release, platform } from "node:os";
import { join } from "node:path";
import { autoUpdater } from "electron-updater";
import { existsSync } from "fs";
import { spawn } from "child_process";
// The built directory structure
//
// ├─┬ dist-electron
// │ ├─┬ main
// │ │ └── index.js > Electron-Main
// │ └─┬ preload
// │ └── index.js > Preload-Scripts
// ├─┬ dist
// │ └── index.html > Electron-Renderer
//
process.env.DIST_ELECTRON = join(__dirname, "..");
process.env.DIST = join(process.env.DIST_ELECTRON, "../dist");
process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL
? join(process.env.DIST_ELECTRON, "../public")
: process.env.DIST;
// Disable GPU Acceleration for Windows 7
if (release().startsWith("6.1")) app.disableHardwareAcceleration();
// Set application name for Windows 10+ notifications
if (process.platform === "win32") app.setAppUserModelId(app.getName());
if (!app.requestSingleInstanceLock()) {
close_sub_processes();
app.quit();
process.exit(0);
}
// Remove electron security warnings
// This warning only shows in development mode
// Read more on https://www.electronjs.org/docs/latest/tutorial/security
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
// set daemon process var
var daemonProcess = null;
let win: BrowserWindow | null = null;
// Here, you can also use other preload
const preload = join(__dirname, "../preload/index.js");
const url = process.env.VITE_DEV_SERVER_URL;
const indexHtml = join(process.env.DIST, "index.html");
async function createWindow() {
win = new BrowserWindow({
title: "FreeDATA",
width: 1200,
height: 670,
icon: join(process.env.VITE_PUBLIC, "icon_cube_border.png"),
autoHideMenuBar: true,
webPreferences: {
preload,
backgroundThrottle: false,
// Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
// Consider using contextBridge.exposeInMainWorld
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
nodeIntegration: true,
contextIsolation: false,
},
});
if (process.env.VITE_DEV_SERVER_URL) {
// electron-vite-vue#298
win.loadURL(url);
// Open devTool if the app is not packaged
win.webContents.openDevTools();
} else {
win.loadFile(indexHtml);
}
// Test actively push message to the Electron-Renderer
win.webContents.on("did-finish-load", () => {
win?.webContents.send("main-process-message", new Date().toLocaleString());
});
// Make all links open with the browser, not with the application
win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith("https:")) shell.openExternal(url);
return { action: "deny" };
});
// win.webContents.on('will-navigate', (event, url) => { }) #344
win.once("ready-to-show", () => {
//autoUpdater.logger = log.scope("updater");
//autoUpdater.channel = config.update_channel;
autoUpdater.autoInstallOnAppQuit = false;
autoUpdater.autoDownload = true;
autoUpdater.checkForUpdatesAndNotify();
//autoUpdater.quitAndInstall();
});
}
//app.whenReady().then(
app.whenReady().then(() => {
createWindow();
//Generate daemon binary path
var daemonPath = "";
switch (platform().toLowerCase()) {
case "darwin":
case "linux":
daemonPath = join(process.resourcesPath, "modem", "freedata-daemon");
break;
case "win32":
case "win64":
daemonPath = join(process.resourcesPath, "modem", "freedata-daemon.exe");
break;
default:
console.log("Unhandled OS Platform: ", platform());
break;
}
//Start daemon binary if it exists
if (existsSync(daemonPath)) {
console.log("Starting freedata-daemon binary");
daemonProcess = spawn(daemonPath, [], {
cwd: join(daemonPath, ".."),
});
// return process messages
daemonProcess.on("error", (err) => {
// daemonProcessLog.error(`error when starting daemon: ${err}`);
console.log(err);
});
daemonProcess.on("message", () => {
// daemonProcessLog.info(`${data}`);
});
daemonProcess.stdout.on("data", () => {
// daemonProcessLog.info(`${data}`);
});
daemonProcess.stderr.on("data", () => {
// daemonProcessLog.info(`${data}`);
});
daemonProcess.on("close", (code) => {
// daemonProcessLog.warn(`daemonProcess exited with code ${code}`);
});
} else {
daemonProcess = null;
daemonPath = null;
console.log("Daemon binary doesn't exist--normal for dev environments.");
}
//)
});
app.on("window-all-closed", () => {
win = null;
if (process.platform !== "darwin") app.quit(close_sub_processes());
});
app.on("second-instance", () => {
if (win) {
// Focus on the main window if the user tried to open another
if (win.isMinimized()) win.restore();
win.focus();
}
});
app.on("activate", () => {
const allWindows = BrowserWindow.getAllWindows();
if (allWindows.length) {
allWindows[0].focus();
} else {
createWindow();
}
});
// New window example arg: new windows url
ipcMain.handle("open-win", (_, arg) => {
const childWindow = new BrowserWindow({
webPreferences: {
preload,
nodeIntegration: true,
contextIsolation: false,
},
});
if (process.env.VITE_DEV_SERVER_URL) {
childWindow.loadURL(`${url}#${arg}`);
} else {
childWindow.loadFile(indexHtml, { hash: arg });
}
});
//restart and install udpate
ipcMain.on("request-restart-and-install-update", (event, data) => {
close_sub_processes();
autoUpdater.quitAndInstall();
});
// LISTENER FOR UPDATER EVENTS
autoUpdater.on("update-available", (info) => {
console.log("update available");
let arg = {
status: "update-available",
info: info,
};
win.webContents.send("action-updater", arg);
});
autoUpdater.on("update-not-available", (info) => {
console.log("update not available");
let arg = {
status: "update-not-available",
info: info,
};
win.webContents.send("action-updater", arg);
});
autoUpdater.on("update-downloaded", (info) => {
console.log("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...
//console.log('quit application and install update');
//autoUpdater.quitAndInstall();
});
autoUpdater.on("checking-for-update", () => {
console.log("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) => {
console.log("update error");
let arg = {
status: "error",
progress: error,
};
win.webContents.send("action-updater", arg);
console.log("AUTO UPDATER : " + error);
});
function close_sub_processes() {
console.log("closing sub processes");
// closing the modem binary if not closed when closing application and also our daemon which has been started by the gui
try {
if (daemonProcess != null) {
daemonProcess.kill();
}
} catch (e) {
console.log(e);
}
console.log("closing modem and daemon");
try {
if (platform() == "win32" || platform() == "win64") {
spawn("Taskkill", ["/IM", "freedata-modem.exe", "/F"]);
spawn("Taskkill", ["/IM", "freedata-daemon.exe", "/F"]);
}
if (platform() == "linux") {
spawn("pkill", ["-9", "freedata-modem"]);
spawn("pkill", ["-9", "freedata-daemon"]);
}
if (platform() == "darwin") {
spawn("pkill", ["-9", "freedata-modem"]);
spawn("pkill", ["-9", "freedata-daemon"]);
}
} catch (e) {
console.log(e);
}
}

View file

@ -0,0 +1,204 @@
import { ipcRenderer } from "electron";
function domReady(
condition: DocumentReadyState[] = ["complete", "interactive"],
) {
return new Promise((resolve) => {
if (condition.includes(document.readyState)) {
resolve(true);
} else {
document.addEventListener("readystatechange", () => {
if (condition.includes(document.readyState)) {
resolve(true);
}
});
}
});
}
const safeDOM = {
append(parent: HTMLElement, child: HTMLElement) {
if (!Array.from(parent.children).find((e) => e === child)) {
return parent.appendChild(child);
}
},
remove(parent: HTMLElement, child: HTMLElement) {
if (Array.from(parent.children).find((e) => e === child)) {
return parent.removeChild(child);
}
},
};
/**
* https://tobiasahlin.com/spinkit
* https://connoratherton.com/loaders
* https://projects.lukehaas.me/css-loaders
* https://matejkustec.github.io/SpinThatShit
*/
function useLoading() {
const className = `loaders-css__square-spin`;
const styleContent = `
@keyframes square-spin {
0% {
transform: rotate(0deg);
background-image: url('icon_cube_border.png'); /* Replace with the URL of your image */
background-size: cover; /* Scale the image to cover the entire container */
}
25% { transform: perspective(100px) rotateX(180deg) rotateY(0);
background-image: url('icon_cube_border.png'); /* Replace with the URL of your image */
background-size: cover; /* Scale the image to cover the entire container */
}
50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg);
background-image: url('icon_cube_border.png'); /* Replace with the URL of your image */
background-size: cover; /* Scale the image to cover the entire container */
}
75% { transform: perspective(100px) rotateX(0) rotateY(180deg);
background-image: url('icon_cube_border.png'); /* Replace with the URL of your image */
background-size: cover; /* Scale the image to cover the entire container */
}
100% { transform: perspective(100px) rotateX(0) rotateY(0);
background-image: url('icon_cube_border.png'); /* Replace with the URL of your image */
background-size: cover; /* Scale the image to cover the entire container */
}
}
.${className} > div {
animation-fill-mode: both;
width: 50px;
height: 50px;
background: #fff;
animation: square-spin 6s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite;
}
.app-loading-wrap {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #282c34;
z-index: 99999;
}
`;
const oStyle = document.createElement("style");
const oDiv = document.createElement("div");
oStyle.id = "app-loading-style";
oStyle.innerHTML = styleContent;
oDiv.className = "app-loading-wrap";
oDiv.innerHTML = `<div class="${className}"><div></div></div>`;
return {
appendLoading() {
safeDOM.append(document.head, oStyle);
safeDOM.append(document.body, oDiv);
},
removeLoading() {
safeDOM.remove(document.head, oStyle);
safeDOM.remove(document.body, oDiv);
},
};
}
// ----------------------------------------------------------------------
const { appendLoading, removeLoading } = useLoading();
domReady().then(appendLoading);
window.onmessage = (ev) => {
ev.data.payload === "removeLoading" && removeLoading();
};
setTimeout(removeLoading, 4999);
window.addEventListener("DOMContentLoaded", () => {
// we are using this area for implementing the electron runUpdater
// we need access to DOM for displaying updater results in GUI
// close app, update and restart
document
.getElementById("update_and_install")
.addEventListener("click", () => {
ipcRenderer.send("request-restart-and-install-update");
});
});
// IPC ACTION FOR AUTO UPDATER
ipcRenderer.on("action-updater", (event, arg) => {
if (arg.status == "download-progress") {
var progressinfo =
"(" +
Math.round(arg.progress.transferred / 1024) +
"kB /" +
Math.round(arg.progress.total / 1024) +
"kB)" +
" @ " +
Math.round(arg.progress.bytesPerSecond / 1024) +
"kByte/s";
document.getElementById("UpdateProgressInfo").innerHTML = progressinfo;
document
.getElementById("UpdateProgressBar")
.setAttribute("aria-valuenow", arg.progress.percent);
document
.getElementById("UpdateProgressBar")
.setAttribute("style", "width:" + arg.progress.percent + "%;");
}
if (arg.status == "checking-for-update") {
//document.title = document.title + ' - v' + arg.version;
//updateTitle(
// config.myCall,
// config.tnc_host,
// config.tnc_port,
// " -v " + arg.version,
//);
document.getElementById("updater_status").innerHTML =
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>';
document.getElementById("updater_status").className =
"btn btn-secondary btn-sm";
document.getElementById("update_and_install").style.display = "none";
}
if (arg.status == "update-downloaded") {
document.getElementById("update_and_install").removeAttribute("style");
document.getElementById("updater_status").innerHTML =
'<i class="bi bi-cloud-download ms-1 me-1" style="color: white;"></i>';
document.getElementById("updater_status").className =
"btn btn-success btn-sm";
// HERE WE NEED TO RUN THIS SOMEHOW...
//mainLog.info('quit application and install update');
//autoUpdater.quitAndInstall();
}
if (arg.status == "update-not-available") {
document.getElementById("updater_last_version").innerHTML =
arg.info.releaseName;
document.getElementById("updater_last_update").innerHTML =
arg.info.releaseDate;
document.getElementById("updater_release_notes").innerHTML =
arg.info.releaseNotes;
document.getElementById("updater_status").innerHTML =
'<i class="bi bi-check2-square ms-1 me-1" style="color: white;"></i>';
document.getElementById("updater_status").className =
"btn btn-success btn-sm";
document.getElementById("update_and_install").style.display = "none";
}
if (arg.status == "update-available") {
document.getElementById("updater_status").innerHTML =
'<i class="bi bi-hourglass-split ms-1 me-1" style="color: white;"></i>';
document.getElementById("updater_status").className =
"btn btn-warning btn-sm";
document.getElementById("update_and_install").style.display = "none";
}
if (arg.status == "error") {
document.getElementById("updater_status").innerHTML =
'<i class="bi bi-exclamation-square ms-1 me-1" style="color: white;"></i>';
document.getElementById("updater_status").className =
"btn btn-danger btn-sm";
document.getElementById("update_and_install").style.display = "none";
}
});

42
gui/index.html Normal file
View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
<title>FreeDATA</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
<script type="module">
// debugging code for figuring out correct folder structure in build environment
console.log(process.env)
import { readdir } from 'node:fs/promises';
import { readdirSync } from 'fs'
import { join } from 'path'
function walk(dir) {
return readdirSync(dir, { withFileTypes: true }).flatMap((file) => file.isDirectory() ? walk(join(dir, file.name)) : join(dir, file.name))
}
if (process.env["NODE_ENV"] == "production"){
console.log(walk(process.env["APPDIR"]))
console.log(walk(process.env["DIST"]))
console.log(walk(process.env["DIST_ELECTRON"]))
} else {
console.log("running in " + process.env["NODE_ENV"])
}
</script>

File diff suppressed because it is too large Load diff

View file

@ -1,36 +1,38 @@
{
"name": "FreeDATA",
"version": "0.10.2-alpha.1",
"description": "FreeDATA ",
"main": "main.js",
"private": true,
"version": "0.11.0-alpha.1",
"main": "dist-electron/main/index.js",
"scripts": {
"start": "electron .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"engines": {
"node": ">=18.17.0",
"npm": ">=9.0.0"
"start": "git pull && npm i && vite",
"dev": "vite",
"check": "vue-tsc --noEmit",
"build": "vue-tsc --noEmit && vite build && electron-builder",
"preview": "vite preview",
"lint": "eslint --ext .js,.vue src",
"lint-fix": "eslint --ext .js,.vue --fix src"
},
"repository": {
"type": "git",
"url": "https://github.com/DJ2LS/FreeDATA.git"
},
"keywords": [
"TNC",
"Modem",
"GUI",
"FreeDATA",
"codec2"
],
"author": "DJ2LS",
"license": "LGPL-2.1",
"license": "GPL-3.0",
"bugs": {
"url": "https://github.com/DJ2LS/FreeDATA/issues"
},
"homepage": "https://freedata.app",
"dependencies": {
"@electron/asar": "^3.2.4",
"@electron/osx-sign": "^1.0.4",
"@electron/notarize": "^2.1.0",
"@popperjs/core": "^2.11.8",
"@vueuse/electron": "^10.4.1",
"blob-util": "^2.0.2",
"bootstrap": "^5.3.1",
"bootstrap-icons": "^1.10.5",
@ -39,72 +41,42 @@
"chart.js": "^4.3.3",
"chartjs-plugin-annotation": "^3.0.1",
"electron-log": "^4.4.8",
"electron-updater": "^6.1.1",
"electron-updater": "^6.1.6",
"emoji-picker-element": "^1.18.3",
"emoji-picker-element-data": "^1.4.0",
"express-pouchdb": "^4.2.0",
"file-saver": "^2.0.5",
"mime": "^3.0.0",
"pinia": "^2.1.6",
"pouchdb": "^8.0.1",
"pouchdb-browser": "^8.0.1",
"pouchdb-express-router": "^0.0.11",
"pouchdb-find": "^8.0.1",
"pouchdb-replication": "^8.0.1",
"pouchdb-upsert": "^2.2.0",
"qth-locator": "^2.1.0",
"utf8": "^3.0.0",
"uuid": "^9.0.0"
"sass": "^1.66.1",
"socket.io": "^4.7.2",
"uuid": "^9.0.0",
"vue": "^3.3.4",
"vue-chartjs": "^5.2.0",
"vuemoji-picker": "^0.2.0"
},
"devDependencies": {
"@electron/notarize": "^2.1.0",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@vitejs/plugin-vue": "^4.4.0",
"electron": "^27.0.0",
"electron-builder": "^24.6.3",
"electron-builder-notarize": "^1.5.1"
},
"build": {
"productName": "FreeDATA",
"appId": "app.freedata",
"afterSign": "electron-builder-notarize",
"npmRebuild": "false",
"directories": {
"buildResources": "build",
"output": "dist"
},
"mac": {
"target": [
"default"
],
"icon": "build/icon.png",
"hardenedRuntime": true,
"entitlements": "build/entitlements.plist",
"entitlementsInherit": "build/entitlements.plist",
"gatekeeperAssess": false
},
"win": {
"icon": "build/icon.png",
"target": [
"nsis"
]
},
"linux": {
"icon": "build/icon.png",
"target": [
"AppImage"
],
"category": "Development"
},
"publish": {
"provider": "github",
"releaseType": "release"
},
"extraResources": [
{
"from": "../tnc/dist/tnc/",
"to": "tnc",
"filter": [
"**/*",
"!**/.git"
]
}
]
"eslint": "^8.50.0",
"eslint-config-prettier": "^9.0.0",
"eslint-config-standard-with-typescript": "^39.1.0",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-n": "^16.1.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-vue": "^9.17.0",
"typescript": "^5.2.2",
"vite": "^4.3.2",
"vite-plugin-electron": "^0.14.0",
"vite-plugin-electron-renderer": "^0.14.5",
"vue": "^3.3.4",
"vue-tsc": "^1.4.2"
}
}

View file

Before

Width:  |  Height:  |  Size: 590 KiB

After

Width:  |  Height:  |  Size: 590 KiB

15
gui/setup.md Normal file
View file

@ -0,0 +1,15 @@
https://getbootstrap.com/docs/5.3/getting-started/vite/
https://vuejs.org/guide/essentials/event-handling.html#inline-handlers
https://linuxhint.com/install-use-bootstrap-with-vue-js/
https://github.com/electron-vite
https://github.com/electron-vite/electron-vite-vue
https://github.com/vuejs/create-vue
https://vue-community.org/guide/ecosystem/desktop-apps.html#electron
https://blog.logrocket.com/building-app-electron-vue/
Folder structure
dist-electron: Automatically compiled source from vite
electron: Source code folder for Electron stuff
public: Public data
src: VueJS source code

View file

@ -1,939 +0,0 @@
var net = require("net");
const path = require("path");
const { ipcRenderer } = require("electron");
const FD = require("./freedata");
const log = require("electron-log");
const socketLog = log.scope("tnc");
//const utf8 = require("utf8");
// 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 client = new net.Socket();
var socketchunk = ""; // Current message, per connection.
// split character
const split_char = "\0;\1;";
// globals for getting new data only if available so we are saving bandwidth
var rxBufferLengthTnc = 0;
var rxBufferLengthGui = 0;
//var rxMsgBufferLengthTnc = 0;
//var rxMsgBufferLengthGui = 0;
// global to keep track of TNC connection error emissions
var tncShowConnectStateError = 1;
// global for storing ip information
var tnc_port = config.tnc_port;
var tnc_host = config.tnc_host;
// network connection Timeout
setTimeout(connectTNC, 2000);
function connectTNC() {
//exports.connectTNC = function(){
//socketLog.info('connecting to TNC...')
//clear message buffer after reconnecting or initial connection
socketchunk = "";
if (config.tnclocation == "localhost") {
client.connect(3000, "127.0.0.1");
} else {
client.connect(tnc_port, tnc_host);
}
}
client.on("connect", function (data) {
socketLog.info("TNC connection established");
let Data = {
busy_state: "-",
arq_state: "-",
//channel_state: "-",
frequency: "-",
mode: "-",
bandwidth: "-",
dbfs_level: 0,
};
ipcRenderer.send("request-update-tnc-state", Data);
// also update tnc connection state
ipcRenderer.send("request-update-tnc-connection", {
tnc_connection: client.readyState,
});
tncShowConnectStateError = 1;
});
client.on("error", function (data) {
if (tncShowConnectStateError == 1) {
socketLog.error("TNC connection error");
tncShowConnectStateError = 0;
}
setTimeout(connectTNC, 500);
client.destroy();
let Data = {
tnc_connection: client.readyState,
busy_state: "-",
arq_state: "-",
//channel_state: "-",
frequency: "-",
mode: "-",
bandwidth: "-",
dbfs_level: 0,
};
ipcRenderer.send("request-update-tnc-state", Data);
ipcRenderer.send("request-update-tnc-connection", {
tnc_connection: client.readyState,
});
});
/*
client.on('close', function(data) {
socketLog.info(' TNC connection closed');
setTimeout(connectTNC, 2000)
});
*/
client.on("end", function (data) {
socketLog.info("TNC connection ended");
ipcRenderer.send("request-update-tnc-connection", {
tnc_connection: client.readyState,
});
client.destroy();
setTimeout(connectTNC, 500);
});
writeTncCommand = function (command) {
//socketLog.info(command)
// we use the writingCommand function to update our TCPIP state because we are calling this function a lot
// if socket opened, we are able to run commands
if (client.readyState == "open") {
client.write(command + "\n");
}
if (client.readyState == "closed") {
socketLog.info("CLOSED!");
}
if (client.readyState == "opening") {
socketLog.info("connecting to TNC...");
}
};
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
*/
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");
//don't think this is needed anymore
//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);
}
}
//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) {
socketLog.info("Throwing away data!!!!\n" + e); // "SyntaxError
//socketLog.info(e); // "SyntaxError
socketLog.info(socketchunk[i]);
socketchunk = "";
//If we're here, I don't think we want to process any data that may be in data variable
continue;
}
}
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 = {
mycallsign: data["mycallsign"],
mygrid: data["mygrid"],
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"],
bandwidth: data["bandwidth"],
dbfs_level: data["audio_dbfs"],
fft: data["fft"],
channel_busy: data["channel_busy"],
channel_busy_slot: data["channel_busy_slot"],
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_seconds_until_finish: data["arq_seconds_until_finish"],
arq_compression_factor: data["arq_compression_factor"],
total_bytes: data["total_bytes"],
arq_transmission_percent: data["arq_transmission_percent"],
stations: data["stations"],
routing_table: data["routing_table"],
mesh_signalling_table: data["mesh_signalling_table"],
beacon_state: data["beacon_state"],
hamlib_status: data["hamlib_status"],
listen: data["listen"],
audio_recording: data["audio_recording"],
speed_list: data["speed_list"],
strength: data["strength"],
is_codec2_traffic: data["is_codec2_traffic"],
//speed_table: [{"bpm" : 5200, "snr": -3, "timestamp":1673555399},{"bpm" : 2315, "snr": 12, "timestamp":1673555500}],
};
ipcRenderer.send("request-update-tnc-state", Data);
//continue to next for loop iteration, nothing else needs to be done here
continue;
}
// ----------- catch tnc messages START -----------
if (data["freedata"] == "tnc-message") {
switch (data["fec"]) {
case "is_writing":
// RX'd FECiswriting
ipcRenderer.send("request-show-fec-toast-iswriting", {
data: [data],
});
break;
case "broadcast":
// RX'd FEC BROADCAST
var encoded_data = FD.atob_FD(data["data"]);
var splitted_data = encoded_data.split(split_char);
var messageArray = [];
if (splitted_data[0] == "m") {
messageArray.push(data);
console.log(data);
}
let Messages = {
data: messageArray,
};
ipcRenderer.send("request-new-msg-received", Messages);
break;
}
switch (data["cq"]) {
case "transmitting":
// CQ TRANSMITTING
ipcRenderer.send("request-show-cq-toast-transmitting", {
data: [data],
});
break;
case "received":
// CQ RECEIVED
ipcRenderer.send("request-show-cq-toast-received", {
data: [data],
});
break;
}
switch (data["qrv"]) {
case "transmitting":
// QRV TRANSMITTING
ipcRenderer.send("request-show-qrv-toast-transmitting", {
data: [data],
});
break;
case "received":
// QRV RECEIVED
ipcRenderer.send("request-show-qrv-toast-received", {
data: [data],
});
break;
}
switch (data["beacon"]) {
case "transmitting":
// BEACON TRANSMITTING
ipcRenderer.send("request-show-beacon-toast-transmitting", {
data: [data],
});
break;
case "received":
// BEACON RECEIVED
ipcRenderer.send("request-show-beacon-toast-received", {
data: [data],
});
ipcRenderer.send("request-new-msg-received", { data: [data] });
break;
}
switch (data["ping"]) {
case "transmitting":
// PING TRANSMITTING
ipcRenderer.send("request-show-ping-toast-transmitting", {
data: [data],
});
break;
case "received":
// PING RECEIVED
ipcRenderer.send("request-show-ping-toast-received", {
data: [data],
});
ipcRenderer.send("request-new-msg-received", { data: [data] });
break;
case "acknowledge":
// PING ACKNOWLEDGE
ipcRenderer.send("request-show-ping-toast-received-ack", {
data: [data],
});
ipcRenderer.send("request-new-msg-received", { data: [data] });
break;
}
// ARQ SESSION && freedata == tnc-message
if (data["arq"] == "session") {
switch (data["status"]) {
case "connecting":
// ARQ Open
ipcRenderer.send("request-show-arq-toast-session-connecting", {
data: [data],
});
break;
case "connected":
// ARQ Opening
ipcRenderer.send("request-show-arq-toast-session-connected", {
data: [data],
});
break;
case "waiting":
// ARQ Opening
ipcRenderer.send("request-show-arq-toast-session-waiting", {
data: [data],
});
break;
case "close":
// ARQ Closing
ipcRenderer.send("request-show-arq-toast-session-close", {
data: [data],
});
break;
case "failed":
// ARQ Failed
ipcRenderer.send("request-show-arq-toast-session-failed", {
data: [data],
});
break;
}
}
// ARQ TRANSMISSION && freedata == tnc-message
if (data["arq"] == "transmission") {
switch (data["status"]) {
case "opened":
// ARQ Open
ipcRenderer.send("request-show-arq-toast-datachannel-opened", {
data: [data],
});
break;
case "opening":
// ARQ Opening IRS/ISS
if (data["irs"] == "False") {
ipcRenderer.send("request-show-arq-toast-datachannel-opening", {
data: [data],
});
ipcRenderer.send("request-update-transmission-status", {
data: [data],
});
} else {
ipcRenderer.send(
"request-show-arq-toast-datachannel-received-opener",
{ data: [data] },
);
ipcRenderer.send("request-update-reception-status", {
data: [data],
});
}
break;
case "waiting":
// ARQ waiting
ipcRenderer.send("request-show-arq-toast-datachannel-waiting", {
data: [data],
});
break;
case "receiving":
// ARQ RX
ipcRenderer.send("request-update-reception-status", {
data: [data],
});
break;
case "failed":
// ARQ TX Failed
if (data["reason"] == "protocol version missmatch") {
ipcRenderer.send(
"request-show-arq-toast-transmission-failed-ver",
{ data: [data] },
);
} else {
ipcRenderer.send("request-show-arq-toast-transmission-failed", {
data: [data],
});
}
switch (data["irs"]) {
case "True":
ipcRenderer.send("request-update-reception-status", {
data: [data],
});
break;
default:
ipcRenderer.send("request-update-transmission-status", {
data: [data],
});
break;
}
break;
case "received":
// ARQ Received
ipcRenderer.send("request-show-arq-toast-transmission-received", {
data: [data],
});
ipcRenderer.send("request-update-reception-status", {
data: [data],
});
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 encoded_data = FD.atob_FD(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);
console.log(data);
}
rxBufferLengthGui = dataArray.length;
let Files = {
data: dataArray,
};
ipcRenderer.send("request-update-rx-buffer", Files);
ipcRenderer.send("request-new-msg-received", Files);
//rxMsgBufferLengthGui = messageArray.length;
let Messages = {
data: messageArray,
};
ipcRenderer.send("request-new-msg-received", Messages);
break;
case "transmitting":
// ARQ transmitting
ipcRenderer.send(
"request-show-arq-toast-transmission-transmitting",
{ data: [data] },
);
ipcRenderer.send("request-update-transmission-status", {
data: [data],
});
break;
case "transmitted":
// ARQ transmitted
ipcRenderer.send(
"request-show-arq-toast-transmission-transmitted",
{ data: [data] },
);
ipcRenderer.send("request-update-transmission-status", {
data: [data],
});
break;
}
}
}
// ----------- catch tnc info messages END -----------
// 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 encoded_data = FD.atob_FD(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);
}
}
//finally delete message buffer
socketchunk = "";
}
});
function hexToBytes(hex) {
for (var bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16));
return bytes;
}
//Get TNC State
exports.getTncState = function () {
command = '{"type" : "get", "command" : "tnc_state"}';
writeTncCommand(command);
};
//Get DATA State
exports.getDataState = function () {
command = '{"type" : "get", "command" : "data_state"}';
//writeTncCommand(command)
};
// Send Ping
exports.sendPing = function (dxcallsign) {
command =
'{"type" : "ping", "command" : "ping", "dxcallsign" : "' +
dxcallsign +
'"}';
writeTncCommand(command);
};
// Send Mesh Ping
exports.sendMeshPing = function (dxcallsign) {
command =
'{"type" : "mesh", "command" : "ping", "dxcallsign" : "' +
dxcallsign +
'"}';
writeTncCommand(command);
};
// Send CQ
exports.sendCQ = function () {
command = '{"type" : "broadcast", "command" : "cqcqcq"}';
writeTncCommand(command);
};
// Set AUDIO Level
exports.setTxAudioLevel = function (value) {
command =
'{"type" : "set", "command" : "tx_audio_level", "value" : "' + value + '"}';
writeTncCommand(command);
};
// Send File
exports.sendFile = function (
dxcallsign,
mode,
frames,
filename,
filetype,
data,
checksum,
) {
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))
//Btoa / atob will not work with charsets > 8 bits (i.e. the emojis); should probably move away from using it
//TODO: Will need to update anyother occurences and throughly test
//data = btoa(data)
data = FD.btoa_FD(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,
uuid,
command,
) {
data = FD.btoa_FD(
"m" +
split_char +
command +
split_char +
checksum +
split_char +
uuid +
split_char +
data,
);
command =
'{"type" : "arq", "command" : "send_raw", "uuid" : "' +
uuid +
'", "parameter" : [{"dxcallsign" : "' +
dxcallsign +
'", "mode" : "' +
mode +
'", "n_frames" : "' +
frames +
'", "data" : "' +
data +
'", "attempts": "10"}]}';
socketLog.info(command);
socketLog.info("-------------------------------------");
writeTncCommand(command);
};
// Send Request message
//It would be then „m + split + request + split + request-type“
function sendRequest(dxcallsign, mode, frames, data, command) {
data = FD.btoa_FD("m" + split_char + command + split_char + data);
command =
'{"type" : "arq", "command" : "send_raw", "parameter" : [{"dxcallsign" : "' +
dxcallsign +
'", "mode" : "' +
mode +
'", "n_frames" : "' +
frames +
'", "data" : "' +
data +
'", "attempts": "10"}]}';
socketLog.info(command);
socketLog.info("--------------REQ--------------------");
writeTncCommand(command);
}
// Send Response message
//It would be then „m + split + request + split + request-type“
function sendResponse(dxcallsign, mode, frames, data, command) {
data = FD.btoa_FD("m" + split_char + command + split_char + data);
command =
'{"type" : "arq", "command" : "send_raw", "parameter" : [{"dxcallsign" : "' +
dxcallsign +
'", "mode" : "' +
mode +
'", "n_frames" : "' +
frames +
'", "data" : "' +
data +
'", "attempts": "10"}]}';
socketLog.info(command);
socketLog.info("--------------RES--------------------");
writeTncCommand(command);
}
//Send station info request
exports.sendRequestInfo = function (dxcallsign) {
//Command 0 = user/station information
//Command 1 = shared folder list
//Command 2 = shared file transfer
sendRequest(dxcallsign, 255, 1, "0", "req");
};
//Send shared folder file list request
exports.sendRequestSharedFolderList = function (dxcallsign) {
//Command 0 = user/station information
//Command 1 = shared folder list
//Command 2 = shared file transfer
sendRequest(dxcallsign, 255, 1, "1", "req");
};
//Send shared file request
exports.sendRequestSharedFile = function (dxcallsign, file) {
//Command 0 = user/station information
//Command 1 = shared folder list
//Command 2 = shared file transfer
sendRequest(dxcallsign, 255, 1, "2" + file, "req");
};
//Send station info response
exports.sendResponseInfo = function (dxcallsign, userinfo) {
//Command 0 = user/station information
//Command 1 = shared folder list
//Command 2 = shared file transfer
sendResponse(dxcallsign, 255, 1, userinfo, "res-0");
};
//Send shared folder response
exports.sendResponseSharedFolderList = function (dxcallsign, sharedFolderList) {
//Command 0 = user/station information
//Command 1 = shared folder list
//Command 2 = shared file transfer
sendResponse(dxcallsign, 255, 1, sharedFolderList, "res-1");
};
//Send shared file response
exports.sendResponseSharedFile = function (
dxcallsign,
sharedFile,
sharedFileData,
) {
console.log(
"In sendResponseSharedFile",
dxcallsign,
sharedFile,
sharedFileData,
);
//Command 0 = user/station information
//Command 1 = shared folder list
//Command 2 = shared file transfer
sendResponse(dxcallsign, 255, 1, sharedFile + "/" + sharedFileData, "res-2");
};
//STOP TRANSMISSION
exports.stopTransmission = function () {
command = '{"type" : "arq", "command": "stop_transmission"}';
writeTncCommand(command);
};
// Get RX BUffer
exports.getRxBuffer = function () {
command = '{"type" : "get", "command" : "rx_buffer"}';
// call command only if new data arrived
if (rxBufferLengthGui != rxBufferLengthTnc) {
writeTncCommand(command);
}
};
// START BEACON
exports.startBeacon = function (interval) {
command =
'{"type" : "broadcast", "command" : "start_beacon", "parameter": "' +
interval +
'"}';
writeTncCommand(command);
};
// STOP BEACON
exports.stopBeacon = function () {
command = '{"type" : "broadcast", "command" : "stop_beacon"}';
writeTncCommand(command);
};
// OPEN ARQ SESSION
exports.connectARQ = function (dxcallsign) {
command =
'{"type" : "arq", "command" : "connect", "dxcallsign": "' +
dxcallsign +
'", "attempts": "10"}';
writeTncCommand(command);
};
// CLOSE ARQ SESSION
exports.disconnectARQ = function () {
command = '{"type" : "arq", "command" : "disconnect"}';
writeTncCommand(command);
};
// SEND TEST FRAME
exports.sendTestFrame = function () {
command = '{"type" : "set", "command" : "send_test_frame"}';
writeTncCommand(command);
};
// SEND FEC
exports.sendFEC = function (mode, payload) {
command =
'{"type" : "fec", "command" : "transmit", "mode" : "' +
mode +
'", "payload" : "' +
payload +
'"}';
writeTncCommand(command);
};
// SEND FEC IS WRITING
exports.sendFecIsWriting = function (mycallsign) {
command =
'{"type" : "fec", "command" : "transmit_is_writing", "mycallsign" : "' +
mycallsign +
'"}';
writeTncCommand(command);
};
// SEND FEC TO BROADCASTCHANNEL
exports.sendBroadcastChannel = function (channel, data_out, uuid) {
let checksum = "";
let command = "";
let data = FD.btoa_FD(
"m" +
split_char +
channel +
//split_char +
//checksum +
split_char +
uuid +
split_char +
data_out,
);
console.log(data.length);
let payload = data;
command =
'{"type" : "fec", "command" : "transmit", "mode": "datac4", "wakeup": "True", "payload" : "' +
payload +
'"}';
writeTncCommand(command);
};
// RECORD AUDIO
exports.record_audio = function () {
command = '{"type" : "set", "command" : "record_audio"}';
writeTncCommand(command);
};
// SET FREQUENCY
exports.set_frequency = function (frequency) {
command =
'{"type" : "set", "command" : "frequency", "frequency": ' + frequency + "}";
writeTncCommand(command);
};
// SET MODE
exports.set_mode = function (mode) {
command = '{"type" : "set", "command" : "mode", "mode": "' + mode + '"}';
console.log(command);
writeTncCommand(command);
};
ipcRenderer.on("action-update-tnc-ip", (event, arg) => {
client.destroy();
let Data = {
busy_state: "-",
arq_state: "-",
//channel_state: "-",
frequency: "-",
mode: "-",
bandwidth: "-",
dbfs_level: 0,
};
ipcRenderer.send("request-update-tnc-state", Data);
tnc_port = arg.port;
tnc_host = arg.adress;
connectTNC();
});
// https://stackoverflow.com/a/50579690
// crc32 calculation
//console.log(crc32('abc'));
//console.log(crc32('abc').toString(16).toUpperCase()); // hex
var crc32 = function (r) {
for (var a, o = [], c = 0; c < 256; c++) {
a = c;
for (var f = 0; f < 8; f++) a = 1 & a ? 3988292384 ^ (a >>> 1) : a >>> 1;
o[c] = a;
}
for (var n = -1, t = 0; t < r.length; t++)
n = (n >>> 8) ^ o[255 & (n ^ r.charCodeAt(t))];
return (-1 ^ n) >>> 0;
};

7
gui/src/App.vue Normal file
View file

@ -0,0 +1,7 @@
<script setup lang="ts">
import FreeDATAMain from "./components/main.vue";
</script>
<template>
<FreeDATAMain />
</template>

File diff suppressed because it is too large Load diff

View file

@ -1,843 +0,0 @@
<!doctype html>
<html lang="en" data-bs-theme="light">
<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"
id="bootstrap_theme"
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>
<!-- bootstrap -->
<script src="../node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<!-- chart.js -->
<script src="../node_modules/chart.js/dist/chart.umd.js"></script>
<!--<script src="../node_modules/chartjs-plugin-annotation/dist/chartjs-plugin-annotation.min.js"></script>-->
<!--<script type="module" src="../node_modules/emoji-picker-element/index.js"></script>-->
<script
type="module"
src="../node_modules/emoji-picker-element/picker.js"
></script>
<script
type="module"
src="../node_modules/emoji-picker-element/database.js"
></script>
<div
class="position-absolute container w-100 h-100 bottom-0 end-0 mb-5"
style="z-index: 100; display: none"
id="emojipickercontainer"
>
<emoji-picker
locale="en"
class="position-absolute bottom-0 end-0 p-1 mb-2"
data-source="../node_modules/emoji-picker-element-data/en/emojibase/data.json"
></emoji-picker>
</div>
<div class="container-fluid">
<div class="row h-100">
<div class="col-4 p-2">
<! ------Chats area ---------------------------------------------------------------------->
<div class="container-fluid m-0 p-0">
<div class="input-group bottom-0 m-0 w-100">
<input
class="form-control w-50"
maxlength="9"
style="text-transform: uppercase"
id="chatModuleNewDxCall"
placeholder="DX CALL"
/>
<button
class="btn btn-sm btn-success"
id="createNewChatButton"
type="button"
title="Start a new chat (enter dx call sign first)"
>
<i class="bi bi-pencil-square" style="font-size: 1.2rem"></i>
</button>
<button
type="button"
id="userModalButton"
data-bs-toggle="modal"
data-bs-target="#userModal"
class="btn btn-sm btn-primary ms-2"
title="My station info"
>
<i class="bi bi-person" style="font-size: 1.2rem"></i>
</button>
<button
type="button"
id="sharedFolderButton"
data-bs-toggle="modal"
data-bs-target="#sharedFolderModal"
class="btn btn-sm btn-primary"
title="My shared folder"
>
<i class="bi bi-files" style="font-size: 1.2rem"></i>
</button>
</div>
</div>
<hr class="m-2" />
<div class="overflow-auto vh-100">
<div
class="list-group overflow-auto"
id="list-tab"
role="tablist"
style="height: calc(100vh - 70px)"
></div>
</div>
</div>
<div class="col-8 border vh-100 p-0">
<! ------ chat navbar ---------------------------------------------------------------------->
<div class="container-fluid m-2 p-0">
<div class="input-group bottom-0">
<button
class="btn btn-sm btn-outline-secondary me"
id="ping"
type="button"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="false"
title="Ping remote station"
>
Ping
</button>
<button
type="button"
id="userModalDXButton"
data-bs-toggle="modal"
data-bs-target="#userModalDX"
class="btn btn-sm btn-outline-secondary"
title="Request remote station's information"
>
<i class="bi bi-person" style="font-size: 1.2rem"></i>
</button>
<button
type="button"
id="sharedFolderDXButton"
data-bs-toggle="modal"
data-bs-target="#sharedFolderModalDX"
class="btn btn-sm btn-outline-secondary me-2"
title="Request remote station's shared files"
>
<i class="bi bi-files" style="font-size: 1.2rem"></i>
</button>
<button
type="button"
class="btn btn-small btn-outline-primary dropdown-toggle me-2"
data-bs-toggle="dropdown"
aria-expanded="false"
data-bs-auto-close="outside"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="false"
title="Message filter"
>
<i class="bi bi-funnel-fill"></i>
</button>
<form class="dropdown-menu p-4" id="frmFilter">
<div class="mb-1">
<div class="form-check">
<input
checked="true"
type="checkbox"
class="form-check-input"
id="chkMessage"
/>
<label class="form-check-label" for="chkMessage">
All Messages
</label>
</div>
</div>
<div class="mb-1">
<div class="form-check">
<input
checked="false"
type="checkbox"
class="form-check-input"
id="chkNewMessage"
/>
<label class="form-check-label" for="chkNewMessage">
Unread Messages
</label>
</div>
</div>
<div class="mb-1">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
id="chkPing"
/>
<label class="form-check-label" for="chkPing">
Pings
</label>
</div>
</div>
<div class="mb-1">
<div class="form-check">
<input
checked="true"
type="checkbox"
class="form-check-input"
id="chkPingAck"
/>
<label class="form-check-label" for="chkPingAck">
Ping-Acks
</label>
</div>
</div>
<div class="mb-1">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
id="chkBeacon"
/>
<label class="form-check-label" for="chkBeacon">
Beacons
</label>
</div>
</div>
<div class="mb-1">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
id="chkRequest"
/>
<label class="form-check-label" for="chkRequest">
Requests
</label>
</div>
</div>
<div class="mb-1">
<div class="form-check">
<input
type="checkbox"
class="form-check-input"
id="chkResponse"
/>
<label class="form-check-label" for="chkResponse">
Responses
</label>
</div>
</div>
<button type="button" class="btn btn-primary" id="btnFilter">
Refresh
</button>
</form>
<button
id="chatSettingsDropDown"
type="button"
class="btn btn-outline-secondary dropdown-toggle"
data-bs-toggle="dropdown"
aria-expanded="false"
title="More options...."
>
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu" aria-labelledby="chatSettingsDropDown">
<li>
<a
class="dropdown-item bg-danger text-white"
id="delete_selected_chat"
href="#"
><i class="bi bi-person-x" style="font-size: 1rem"></i>
Delete chat</a
>
</li>
<div class="dropdown-divider"></div>
<li>
<button
class="dropdown-item"
id="openHelpModalchat"
data-bs-toggle="modal"
data-bs-target="#chatHelpModal"
>
<i
class="bi bi-question-circle"
style="font-size: 1rem"
></i>
Help
</button>
</li>
</ul>
<span
class="input-group-text ms-2"
id="txtConnectedWithChat"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
title="Connected with"
>------</span
>
</div>
</div>
<hr class="m-0" />
<! ------messages area ---------------------------------------------------------------------->
<div
class="container overflow-auto"
id="message-container"
style="height: calc(100% - 150px)"
>
<div class="tab-content" id="nav-tabContent"></div>
<!--<div class="container position-absolute bottom-0">-->
</div>
<!-- </div>-->
<div class="container-fluid mt-2 p-0">
<input
type="checkbox"
id="expand_textarea"
class="btn-check"
autocomplete="off"
/>
<label
class="btn d-flex justify-content-center"
id="expand_textarea_label"
for="expand_textarea"
><i
id="expand_textarea_button"
class="bi bi-chevron-compact-up"
></i
></label>
<div class="input-group bottom-0 ms-2">
<!--<input class="form-control" maxlength="8" style="max-width: 6rem; text-transform:uppercase; display:none" id="chatModuleDxCall" placeholder="DX CALL"></input>-->
<!--<button class="btn btn-sm btn-primary me-2" id="emojipickerbutton" type="button">-->
<div class="input-group-text">
<i
id="emojipickerbutton"
class="bi bi-emoji-smile p-0"
style="font-size: 1rem"
></i>
</div>
<textarea
class="form-control"
rows="1"
id="chatModuleMessage"
placeholder="Message - Send with [Enter]"
></textarea>
<div class="input-group-text me-3">
<i
class="bi bi-paperclip"
style="font-size: 1rem"
id="selectFilesButton"
></i>
<button
class="btn btn-sm btn-secondary d-none invisible"
id="sendMessage"
type="button"
>
<i class="bi bi-send" style="font-size: 1.2rem"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- user modal -->
<div
class="modal fade"
id="userModal"
tabindex="-1"
aria-labelledby="userModalLabel"
aria-hidden="true"
>
<div class="modal-dialog" style="max-width: 600px">
<div class="modal-content">
<div class="card mb-1 border-0">
<div class="row g-0">
<div class="col-md-4">
<div class="row position-relative p-0 m-0">
<div class="col p-0 m-0">
<img
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iY3VycmVudENvbG9yIiBjbGFzcz0iYmkgYmktcGVyc29uLWJvdW5kaW5nLWJveCIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8cGF0aCBkPSJNMS41IDFhLjUuNSAwIDAgMC0uNS41djNhLjUuNSAwIDAgMS0xIDB2LTNBMS41IDEuNSAwIDAgMSAxLjUgMGgzYS41LjUgMCAwIDEgMCAxaC0zek0xMSAuNWEuNS41IDAgMCAxIC41LS41aDNBMS41IDEuNSAwIDAgMSAxNiAxLjV2M2EuNS41IDAgMCAxLTEgMHYtM2EuNS41IDAgMCAwLS41LS41aC0zYS41LjUgMCAwIDEtLjUtLjV6TS41IDExYS41LjUgMCAwIDEgLjUuNXYzYS41LjUgMCAwIDAgLjUuNWgzYS41LjUgMCAwIDEgMCAxaC0zQTEuNSAxLjUgMCAwIDEgMCAxNC41di0zYS41LjUgMCAwIDEgLjUtLjV6bTE1IDBhLjUuNSAwIDAgMSAuNS41djNhMS41IDEuNSAwIDAgMS0xLjUgMS41aC0zYS41LjUgMCAwIDEgMC0xaDNhLjUuNSAwIDAgMCAuNS0uNXYtM2EuNS41IDAgMCAxIC41LS41eiIvPgogIDxwYXRoIGQ9Ik0zIDE0cy0xIDAtMS0xIDEtNCA2LTQgNiAzIDYgNC0xIDEtMSAxSDN6bTgtOWEzIDMgMCAxIDEtNiAwIDMgMyAwIDAgMSA2IDB6Ii8+Cjwvc3ZnPg=="
class="img-fluid rounded-start w-100"
alt="..."
id="user_info_image"
/>
</div>
<div
class="col position-absolute image-overlay text-white justify-content-center align-items-center d-flex align-middle h-100 opacity-0"
id="userImageSelector"
>
<i class="bi bi-upload" style="font-size: 2.2rem"></i>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card-body">
<div class="input-group input-group-sm mb-1">
<span class="input-group-text"
><i class="bi bi-pass"></i
></span>
<input
type="text"
class="form-control"
placeholder="Callsign"
id="user_info_callsign"
aria-label="Call"
aria-describedby="basic-addon1"
/>
<span class="input-group-text"
><i class="bi bi-person-vcard"></i
></span>
<input
type="text"
class="form-control"
placeholder="name"
id="user_info_name"
aria-label="Name"
aria-describedby="basic-addon1"
/>
<span class="input-group-text"
><i class="bi bi-sunrise"></i
></span>
<input
type="text"
class="form-control"
placeholder="age"
id="user_info_age"
aria-label="age"
aria-describedby="basic-addon1"
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text"
><i class="bi bi-house"></i
></span>
<input
type="text"
class="form-control"
placeholder="Location"
id="user_info_location"
aria-label="Name"
aria-describedby="basic-addon1"
/>
<span class="input-group-text"
><i class="bi bi-pin-map"></i
></span>
<input
type="text"
class="form-control"
placeholder="Grid"
id="user_info_gridsquare"
aria-label="Name"
aria-describedby="basic-addon1"
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text"
><i class="bi bi-projector"></i
></span>
<input
type="text"
class="form-control"
placeholder="Radio"
id="user_info_radio"
aria-label="Name"
aria-describedby="basic-addon1"
/>
<span class="input-group-text"
><i class="bi bi-broadcast-pin"></i
></span>
<input
type="text"
class="form-control"
placeholder="Antenna"
id="user_info_antenna"
aria-label="Name"
aria-describedby="basic-addon1"
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text"
><i class="bi bi-envelope"></i
></span>
<input
type="text"
class="form-control"
placeholder="Email"
id="user_info_email"
aria-label="Name"
aria-describedby="basic-addon1"
/>
<span class="input-group-text"
><i class="bi bi-globe"></i
></span>
<input
type="text"
class="form-control"
placeholder="Website"
id="user_info_website"
aria-label="Name"
aria-describedby="basic-addon1"
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text"
><i class="bi bi-info-circle"></i
></span>
<input
type="text"
class="form-control"
placeholder="Comments"
id="user_info_comments"
aria-label="Comments"
aria-describedby="basic-addon1"
/>
</div>
</div>
</div>
</div>
</div>
<button
type="button"
class="btn btn-primary"
data-bs-dismiss="modal"
aria-label="Close"
id="userInfoSave"
>
Save & Close
</button>
</div>
</div>
</div>
<!-- dx user modal -->
<div
class="modal fade"
id="userModalDX"
tabindex="-1"
aria-labelledby="userModalDXLabel"
aria-hidden="true"
>
<div class="modal-dialog" style="max-width: 600px">
<div class="modal-content">
<div class="card mb-1 border-0">
<div class="row g-0">
<div class="col-md-4">
<img
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iY3VycmVudENvbG9yIiBjbGFzcz0iYmkgYmktcGVyc29uLWJvdW5kaW5nLWJveCIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8cGF0aCBkPSJNMS41IDFhLjUuNSAwIDAgMC0uNS41djNhLjUuNSAwIDAgMS0xIDB2LTNBMS41IDEuNSAwIDAgMSAxLjUgMGgzYS41LjUgMCAwIDEgMCAxaC0zek0xMSAuNWEuNS41IDAgMCAxIC41LS41aDNBMS41IDEuNSAwIDAgMSAxNiAxLjV2M2EuNS41IDAgMCAxLTEgMHYtM2EuNS41IDAgMCAwLS41LS41aC0zYS41LjUgMCAwIDEtLjUtLjV6TS41IDExYS41LjUgMCAwIDEgLjUuNXYzYS41LjUgMCAwIDAgLjUuNWgzYS41LjUgMCAwIDEgMCAxaC0zQTEuNSAxLjUgMCAwIDEgMCAxNC41di0zYS41LjUgMCAwIDEgLjUtLjV6bTE1IDBhLjUuNSAwIDAgMSAuNS41djNhMS41IDEuNSAwIDAgMS0xLjUgMS41aC0zYS41LjUgMCAwIDEgMC0xaDNhLjUuNSAwIDAgMCAuNS0uNXYtM2EuNS41IDAgMCAxIC41LS41eiIvPgogIDxwYXRoIGQ9Ik0zIDE0cy0xIDAtMS0xIDEtNCA2LTQgNiAzIDYgNC0xIDEtMSAxSDN6bTgtOWEzIDMgMCAxIDEtNiAwIDMgMyAwIDAgMSA2IDB6Ii8+Cjwvc3ZnPg=="
class="img-fluid rounded-start w-100"
alt="..."
id="dx_user_info_image"
/>
</div>
<div class="col-md-8">
<div class="card-body">
<h5>
<span
class="badge bg-secondary"
id="dx_user_info_callsign"
></span>
-
<span
class="badge bg-secondary"
id="dx_user_info_name"
></span>
<span
class="badge bg-secondary"
id="dx_user_info_age"
></span>
</h5>
<ul class="card-text list-unstyled">
<li>
<strong class="col"><i class="bi bi-house"></i> </strong
><span id="dx_user_info_location"></span> (<span
id="dx_user_info_gridsquare"
></span
>)
</li>
<li>
<strong class="col"
><i class="bi bi-envelope"></i> </strong
><span id="dx_user_info_email"></span>
</li>
<li>
<strong class="col"><i class="bi bi-globe"></i> </strong
><span id="dx_user_info_website"></span>
</li>
<li>
<strong class="col"
><i class="bi bi-broadcast-pin"></i> </strong
><span id="dx_user_info_antenna"></span>
</li>
<li>
<strong class="col"
><i class="bi bi-projector"></i> </strong
><span id="dx_user_info_radio"></span>
</li>
<li>
<strong class="col"
><i class="bi bi-info-circle"></i> </strong
><span id="dx_user_info_comments"></span>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="input-group input-group-sm m-0 p-0">
<button
type="button"
class="btn btn-warning w-75"
aria-label="Request"
id="requestUserInfo"
>
Request user data (about 20kBytes!)
</button>
<button
type="button"
class="btn btn-primary w-25"
data-bs-dismiss="modal"
aria-label="Close"
>
Close
</button>
</div>
</div>
</div>
</div>
<!-- user shared folder -->
<div
class="modal fade"
id="sharedFolderModal"
tabindex="-1"
aria-labelledby="sharedFolderModalLabel"
aria-hidden="true"
>
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="sharedFolderModalLabel">
My Shared folder
<button
type="button"
class="btn btn-primary"
id="openSharedFilesFolder"
>
<i class="bi bi-archive"></i>
</button>
</h1>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div class="container-fluid p-0">
<div class="center mb-1">
<div class="badge text-bg-info">
<i class="bi bi-info"></i> Change folder in settings!
</div>
</div>
<div class="table-responsive">
<!-- START OF TABLE FOR SHARED FOLDER -->
<table
class="table table-sm table-hover table-bordered align-middle"
>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
<th scope="col">Type</th>
<th scope="col">Size</th>
</tr>
</thead>
<tbody id="sharedFolderTable"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- HELP MODAL -->
<div
class="modal fade"
data-bs-backdrop="static"
tabindex="-1"
id="chatHelpModal"
>
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Chat Help</h5>
<button
type="button"
class="btn btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div class="card mb-3">
<div class="card-body">
<p class="card-text">
Welcome to the chat window. Heard stations are listed in the
list on the left. Clicking on a station will show messages
sent and/or received from the selected station. Additional
help is available on various extra features below.
</p>
</div>
</div>
<div class="card mb-3">
<div class="card-body">
<button type="button" class="btn btn-sm btn-primary ms-2">
<i class="bi bi-person" style="font-size: 1.2rem"></i>
</button>
<p class="card-text">
Set your station information and picture. This information can
be requested by a remote station and can be enabled/disabled
via settings.
</p>
</div>
</div>
<div class="card mb-3">
<div class="card-body">
<button
type="button"
class="btn btn-sm btn-outline-secondary ms-2"
>
<i class="bi bi-person" style="font-size: 1.2rem"></i>
</button>
<p class="card-text">
Request the selected station's information.
</p>
</div>
</div>
<div class="card mb-3">
<div class="card-body">
<button
type="button"
class="btn btn-sm btn-outline-secondary ms-2"
>
<i class="bi bi-files" style="font-size: 1.2rem"></i>
</button>
<p class="card-text">
Request the selected station's shared file(s) list. Clicking
<button type="button" class="btn btn-sm btn-primary ms-2">
<i class="bi bi-files" style="font-size: 1.2rem"></i>
</button>
will allow you to preview your shared files. Shared file can
be enabled/disabled in settings.
</p>
</div>
</div>
<div class="card mb-3">
<div class="card-body">
<button
type="button"
class="btn btn-small btn-outline-primary dropdown-toggle me-2"
>
<i class="bi bi-funnel-fill"></i>
</button>
<p class="card-text">
The filter button allows you to show or hide certain types of
messages. A lot of data is logged and this allows you to
modify what is shown. By default sent and received messages
and ping acknowlegements are displayed.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- dx user shared folder -->
<div
class="modal fade"
id="sharedFolderModalDX"
tabindex="-1"
aria-labelledby="sharedFolderModalDXLabel"
aria-hidden="true"
>
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="sharedFolderModalDXLabel">
Shared folder
</h1>
<button
type="button"
class="btn btn-primary m-2"
aria-label="Request"
id="requestSharedFolderList"
>
<i class="bi bi-arrow-repeat"></i>
</button>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div class="container-fluid">
<div class="table-responsive">
<!-- START OF TABLE FOR SHARED FOLDER DX -->
<table
class="table table-sm table-hover table-bordered align-middle"
>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
<th scope="col">Type</th>
<th scope="col">Size</th>
</tr>
</thead>
<tbody id="sharedFolderTableDX"></tbody>
</table>
</div>
</div>
<div class="modal-footer">
<div class="input-group input-group-sm m-0 p-0"></div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

523
gui/src/components/chat.vue Normal file
View file

@ -0,0 +1,523 @@
<script setup lang="ts">
import chat_navbar from "./chat_navbar.vue";
import chat_conversations from "./chat_conversations.vue";
import chat_messages from "./chat_messages.vue";
import chat_new_message from "./chat_new_message.vue";
//updateAllChat();
</script>
<template>
<div class="container-fluid m-0 p-0">
<!------ chat navbar ---------------------------------------------------------------------->
<chat_navbar />
<div class="row h-100 ms-0 mt-0 me-1">
<div class="col-3 m-0 p-0 h-100 bg-light">
<!------Chats area ---------------------------------------------------------------------->
<div class="container-fluid vh-100 overflow-scroll m-0 p-0">
<chat_conversations />
</div>
<div class="h-100">
<div
class="list-group overflow-auto"
id="list-tab-chat"
role="tablist"
style="height: calc(100vh - 70px)"
></div>
</div>
</div>
<div class="col-9 border-start vh-100 p-0">
<!------messages area ---------------------------------------------------------------------->
<div
class="container overflow-auto"
id="message-container"
style="height: calc(100% - 200px)"
>
<chat_messages />
</div>
<!------ new message area ---------------------------------------------------------------------->
<chat_new_message />
</div>
</div>
</div>
<!-- user modal -->
<div
class="modal fade"
id="userModal"
tabindex="-1"
aria-labelledby="userModalLabel"
aria-hidden="true"
>
<div class="modal-dialog" style="max-width: 600px">
<div class="modal-content">
<div class="card mb-1 border-0">
<div class="row g-0">
<div class="col-md-4">
<div class="row position-relative p-0 m-0">
<div class="col p-0 m-0">
<img
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iY3VycmVudENvbG9yIiBjbGFzcz0iYmkgYmktcGVyc29uLWJvdW5kaW5nLWJveCIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8cGF0aCBkPSJNMS41IDFhLjUuNSAwIDAgMC0uNS41djNhLjUuNSAwIDAgMS0xIDB2LTNBMS41IDEuNSAwIDAgMSAxLjUgMGgzYS41LjUgMCAwIDEgMCAxaC0zek0xMSAuNWEuNS41IDAgMCAxIC41LS41aDNBMS41IDEuNSAwIDAgMSAxNiAxLjV2M2EuNS41IDAgMCAxLTEgMHYtM2EuNS41IDAgMCAwLS41LS41aC0zYS41LjUgMCAwIDEtLjUtLjV6TS41IDExYS41LjUgMCAwIDEgLjUuNXYzYS41LjUgMCAwIDAgLjUuNWgzYS41LjUgMCAwIDEgMCAxaC0zQTEuNSAxLjUgMCAwIDEgMCAxNC41di0zYS41LjUgMCAwIDEgLjUtLjV6bTE1IDBhLjUuNSAwIDAgMSAuNS41djNhMS41IDEuNSAwIDAgMS0xLjUgMS41aC0zYS41LjUgMCAwIDEgMC0xaDNhLjUuNSAwIDAgMCAuNS0uNXYtM2EuNS41IDAgMCAxIC41LS41eiIvPgogIDxwYXRoIGQ9Ik0zIDE0cy0xIDAtMS0xIDEtNCA2LTQgNiAzIDYgNC0xIDEtMSAxSDN6bTgtOWEzIDMgMCAxIDEtNiAwIDMgMyAwIDAgMSA2IDB6Ii8+Cjwvc3ZnPg=="
class="img-fluid rounded-start w-100"
alt="..."
id="user_info_image"
/>
</div>
<div
class="col position-absolute image-overlay text-white justify-content-center align-items-center d-flex align-middle h-100 opacity-0"
id="userImageSelector"
>
<i class="bi bi-upload" style="font-size: 2.2rem"></i>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card-body">
<div class="input-group input-group-sm mb-1">
<span class="input-group-text"
><i class="bi bi-pass"></i
></span>
<input
type="text"
class="form-control"
placeholder="Callsign"
id="user_info_callsign"
aria-label="Call"
aria-describedby="basic-addon1"
/>
<span class="input-group-text"
><i class="bi bi-person-vcard"></i
></span>
<input
type="text"
class="form-control"
placeholder="name"
id="user_info_name"
aria-label="Name"
aria-describedby="basic-addon1"
/>
<span class="input-group-text"
><i class="bi bi-sunrise"></i
></span>
<input
type="text"
class="form-control"
placeholder="age"
id="user_info_age"
aria-label="age"
aria-describedby="basic-addon1"
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text"
><i class="bi bi-house"></i
></span>
<input
type="text"
class="form-control"
placeholder="Location"
id="user_info_location"
aria-label="Name"
aria-describedby="basic-addon1"
/>
<span class="input-group-text"
><i class="bi bi-pin-map"></i
></span>
<input
type="text"
class="form-control"
placeholder="Grid"
id="user_info_gridsquare"
aria-label="Name"
aria-describedby="basic-addon1"
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text"
><i class="bi bi-projector"></i
></span>
<input
type="text"
class="form-control"
placeholder="Radio"
id="user_info_radio"
aria-label="Name"
aria-describedby="basic-addon1"
/>
<span class="input-group-text"
><i class="bi bi-broadcast-pin"></i
></span>
<input
type="text"
class="form-control"
placeholder="Antenna"
id="user_info_antenna"
aria-label="Name"
aria-describedby="basic-addon1"
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text"
><i class="bi bi-envelope"></i
></span>
<input
type="text"
class="form-control"
placeholder="Email"
id="user_info_email"
aria-label="Name"
aria-describedby="basic-addon1"
/>
<span class="input-group-text"
><i class="bi bi-globe"></i
></span>
<input
type="text"
class="form-control"
placeholder="Website"
id="user_info_website"
aria-label="Name"
aria-describedby="basic-addon1"
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text"
><i class="bi bi-info-circle"></i
></span>
<input
type="text"
class="form-control"
placeholder="Comments"
id="user_info_comments"
aria-label="Comments"
aria-describedby="basic-addon1"
/>
</div>
</div>
</div>
</div>
</div>
<button
type="button"
class="btn btn-primary"
data-bs-dismiss="modal"
aria-label="Close"
id="userInfoSave"
>
Save & Close
</button>
</div>
</div>
</div>
<!-- dx user modal -->
<div
class="modal fade"
id="userModalDX"
tabindex="-1"
aria-labelledby="userModalDXLabel"
aria-hidden="true"
>
<div class="modal-dialog" style="max-width: 600px">
<div class="modal-content">
<div class="card mb-1 border-0">
<div class="row g-0">
<div class="col-md-4">
<img
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iY3VycmVudENvbG9yIiBjbGFzcz0iYmkgYmktcGVyc29uLWJvdW5kaW5nLWJveCIgdmlld0JveD0iMCAwIDE2IDE2Ij4KICA8cGF0aCBkPSJNMS41IDFhLjUuNSAwIDAgMC0uNS41djNhLjUuNSAwIDAgMS0xIDB2LTNBMS41IDEuNSAwIDAgMSAxLjUgMGgzYS41LjUgMCAwIDEgMCAxaC0zek0xMSAuNWEuNS41IDAgMCAxIC41LS41aDNBMS41IDEuNSAwIDAgMSAxNiAxLjV2M2EuNS41IDAgMCAxLTEgMHYtM2EuNS41IDAgMCAwLS41LS41aC0zYS41LjUgMCAwIDEtLjUtLjV6TS41IDExYS41LjUgMCAwIDEgLjUuNXYzYS41LjUgMCAwIDAgLjUuNWgzYS41LjUgMCAwIDEgMCAxaC0zQTEuNSAxLjUgMCAwIDEgMCAxNC41di0zYS41LjUgMCAwIDEgLjUtLjV6bTE1IDBhLjUuNSAwIDAgMSAuNS41djNhMS41IDEuNSAwIDAgMS0xLjUgMS41aC0zYS41LjUgMCAwIDEgMC0xaDNhLjUuNSAwIDAgMCAuNS0uNXYtM2EuNS41IDAgMCAxIC41LS41eiIvPgogIDxwYXRoIGQ9Ik0zIDE0cy0xIDAtMS0xIDEtNCA2LTQgNiAzIDYgNC0xIDEtMSAxSDN6bTgtOWEzIDMgMCAxIDEtNiAwIDMgMyAwIDAgMSA2IDB6Ii8+Cjwvc3ZnPg=="
class="img-fluid rounded-start w-100"
alt="..."
id="dx_user_info_image"
/>
</div>
<div class="col-md-8">
<div class="card-body">
<h5>
<span
class="badge bg-secondary"
id="dx_user_info_callsign"
></span>
-
<span
class="badge bg-secondary"
id="dx_user_info_name"
></span>
<span class="badge bg-secondary" id="dx_user_info_age"></span>
</h5>
<ul class="card-text list-unstyled">
<li>
<strong class="col"><i class="bi bi-house"></i> </strong
><span id="dx_user_info_location"></span> (<span
id="dx_user_info_gridsquare"
></span
>)
</li>
<li>
<strong class="col"><i class="bi bi-envelope"></i> </strong
><span id="dx_user_info_email"></span>
</li>
<li>
<strong class="col"><i class="bi bi-globe"></i> </strong
><span id="dx_user_info_website"></span>
</li>
<li>
<strong class="col"
><i class="bi bi-broadcast-pin"></i> </strong
><span id="dx_user_info_antenna"></span>
</li>
<li>
<strong class="col"><i class="bi bi-projector"></i> </strong
><span id="dx_user_info_radio"></span>
</li>
<li>
<strong class="col"
><i class="bi bi-info-circle"></i> </strong
><span id="dx_user_info_comments"></span>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="input-group input-group-sm m-0 p-0">
<button
type="button"
class="btn btn-warning w-75"
aria-label="Request"
id="requestUserInfo"
>
Request user data (about 20kBytes!)
</button>
<button
type="button"
class="btn btn-primary w-25"
data-bs-dismiss="modal"
aria-label="Close"
>
Close
</button>
</div>
</div>
</div>
</div>
<!-- user shared folder -->
<div
class="modal fade"
id="sharedFolderModal"
tabindex="-1"
aria-labelledby="sharedFolderModalLabel"
aria-hidden="true"
>
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="sharedFolderModalLabel">
My Shared folder
<button
type="button"
class="btn btn-primary"
id="openSharedFilesFolder"
>
<i class="bi bi-archive"></i>
</button>
</h1>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div class="container-fluid p-0">
<div class="center mb-1">
<div class="badge text-bg-info">
<i class="bi bi-info"></i> Change folder in settings!
</div>
</div>
<div class="table-responsive">
<!-- START OF TABLE FOR SHARED FOLDER -->
<table
class="table table-sm table-hover table-bordered align-middle"
>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
<th scope="col">Type</th>
<th scope="col">Size</th>
</tr>
</thead>
<tbody id="sharedFolderTable"></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- HELP MODAL -->
<div
class="modal fade"
data-bs-backdrop="static"
tabindex="-1"
id="chatHelpModal"
>
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Chat Help</h5>
<button
type="button"
class="btn btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div class="card mb-3">
<div class="card-body">
<p class="card-text">
Welcome to the chat window. Heard stations are listed in the
list on the left. Clicking on a station will show messages sent
and/or received from the selected station. Additional help is
available on various extra features below.
</p>
</div>
</div>
<div class="card mb-3">
<div class="card-body">
<button type="button" class="btn btn-sm btn-primary ms-2">
<i class="bi bi-person" style="font-size: 1.2rem"></i>
</button>
<p class="card-text">
Set your station information and picture. This information can
be requested by a remote station and can be enabled/disabled via
settings.
</p>
</div>
</div>
<div class="card mb-3">
<div class="card-body">
<button
type="button"
class="btn btn-sm btn-outline-secondary ms-2"
>
<i class="bi bi-person" style="font-size: 1.2rem"></i>
</button>
<p class="card-text">
Request the selected station's information.
</p>
</div>
</div>
<div class="card mb-3">
<div class="card-body">
<button
type="button"
class="btn btn-sm btn-outline-secondary ms-2"
>
<i class="bi bi-files" style="font-size: 1.2rem"></i>
</button>
<p class="card-text">
Request the selected station's shared file(s) list. Clicking
<button type="button" class="btn btn-sm btn-primary ms-2">
<i class="bi bi-files" style="font-size: 1.2rem"></i>
</button>
will allow you to preview your shared files. Shared file can be
enabled/disabled in settings.
</p>
</div>
</div>
<div class="card mb-3">
<div class="card-body">
<button
type="button"
class="btn btn-small btn-outline-primary dropdown-toggle me-2"
>
<i class="bi bi-funnel-fill"></i>
</button>
<p class="card-text">
The filter button allows you to show or hide certain types of
messages. A lot of data is logged and this allows you to modify
what is shown. By default sent and received messages and ping
acknowlegements are displayed.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- dx user shared folder -->
<div
class="modal fade"
id="sharedFolderModalDX"
tabindex="-1"
aria-labelledby="sharedFolderModalDXLabel"
aria-hidden="true"
>
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="sharedFolderModalDXLabel">
Shared folder
</h1>
<button
type="button"
class="btn btn-primary m-2"
aria-label="Request"
id="requestSharedFolderList"
>
<i class="bi bi-arrow-repeat"></i>
</button>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div class="container-fluid">
<div class="table-responsive">
<!-- START OF TABLE FOR SHARED FOLDER DX -->
<table
class="table table-sm table-hover table-bordered align-middle"
>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
<th scope="col">Type</th>
<th scope="col">Size</th>
</tr>
</thead>
<tbody id="sharedFolderTableDX"></tbody>
</table>
</div>
</div>
<div class="modal-footer">
<div class="input-group input-group-sm m-0 p-0"></div>
</div>
</div>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,84 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useChatStore } from "../store/chatStore.js";
const chat = useChatStore(pinia);
import {
getNewMessagesByDXCallsign,
resetIsNewMessage,
} from "../js/chatHandler";
function chatSelected(callsign) {
chat.selectedCallsign = callsign.toUpperCase();
// scroll message container to bottom
var messageBody = document.getElementById("message-container");
if (messageBody != null) {
// needs sensible defaults
messageBody.scrollTop = messageBody.scrollHeight - messageBody.clientHeight;
}
if (getNewMessagesByDXCallsign(callsign)[1] > 0) {
let messageArray = getNewMessagesByDXCallsign(callsign)[2];
console.log(messageArray);
for (const key in messageArray) {
resetIsNewMessage(messageArray[key].uuid, false);
}
}
try {
chat.beaconLabelArray = Object.values(
chat.sorted_beacon_list[chat.selectedCallsign].timestamp,
);
chat.beaconDataArray = Object.values(
chat.sorted_beacon_list[chat.selectedCallsign].snr,
);
} catch (e) {
console.log("beacon data not fetched: " + e);
chat.beaconLabelArray = [];
chat.beaconDataArray = [];
}
}
</script>
<template>
<div class="list-group m-0 p-0" id="chat-list-tab" role="chat-tablist">
<template v-for="(item, key) in chat.callsign_list" :key="item.dxcallsign">
<a
class="list-group-item list-group-item-action border-0 border-bottom rounded-0"
:class="{ active: key == 0 }"
:id="`list-chat-list-${item}`"
data-bs-toggle="list"
:href="`#list-${item}-messages`"
role="tab"
aria-controls="list-{{item}}-messages"
@click="chatSelected(item)"
>
<div class="row">
<div class="col-9">
{{ item }}
<span
class="badge rounded-pill bg-danger"
v-if="getNewMessagesByDXCallsign(item)[1] > 0"
>
{{ getNewMessagesByDXCallsign(item)[1] }} new messages
</span>
</div>
<div class="col-3">
<button
class="btn btn-sm btn-outline-secondary ms-2 border-0"
data-bs-target="#deleteChatModal"
data-bs-toggle="modal"
@click="chatSelected(item)"
>
<i class="bi bi-three-dots-vertical"></i>
</button>
</div>
</div>
</a>
</template>
</div>
</template>

View file

@ -0,0 +1,102 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useChatStore } from "../store/chatStore.js";
const chat = useChatStore(pinia);
import SentMessage from "./chat_messages_sent.vue"; // Import the chat_messages_sent component
import ReceivedMessage from "./chat_messages_received.vue"; // Import the chat_messages_sent component
import ReceivedBroadcastMessage from "./chat_messages_broadcast_received.vue"; // Import the chat_messages_sent component for broadcasts
import SentBroadcastMessage from "./chat_messages_broadcast_sent.vue"; // Import the chat_messages_sent component for broadcasts
//helper function for saving the last messages day for disaplying the day based divider
var prevChatMessageDay = "";
function getDateTime(timestampRaw) {
var datetime = new Date(timestampRaw * 1000).toLocaleString(
navigator.language,
{
hourCycle: "h23",
year: "numeric",
month: "2-digit",
day: "2-digit",
},
);
return datetime;
}
</script>
<template>
<div class="tab-content" id="nav-tabContent-chat-messages">
<template v-for="(callsign, key) in chat.callsign_list">
<div
class="tab-pane fade show"
:class="{ active: key == 0 }"
:id="`list-${callsign}-messages`"
role="tabpanel"
:aria-labelledby="`list-chat-list-${callsign}`"
>
<template
v-for="item in chat.sorted_chat_list[callsign]"
:key="item._id"
>
<div v-if="prevChatMessageDay !== getDateTime(item.timestamp)">
<div class="separator my-2">
{{ (prevChatMessageDay = getDateTime(item.timestamp)) }}
</div>
</div>
<div v-if="item.type === 'beacon' && item.status === 'received'">
<!-- {{ item }} -->
</div>
<div v-if="item.type === 'ping'">{{ item.snr }} dB ping received</div>
<div v-if="item.type === 'ping-ack'">
{{ item.snr }} dB ping-ack received
</div>
<div v-if="item.type === 'transmit'">
<sent-message :message="item" />
</div>
<div v-else-if="item.type === 'received'">
<received-message :message="item" />
</div>
<div v-if="item.type === 'broadcast_transmit'">
<sent-broadcast-message :message="item" />
</div>
<div v-else-if="item.type === 'broadcast_received'">
<received-broadcast-message :message="item" />
</div>
</template>
</div>
</template>
</div>
</template>
<style>
/* https://stackoverflow.com/a/26634224 */
.separator {
display: flex;
align-items: center;
text-align: center;
color: #6c757d;
}
.separator::before,
.separator::after {
content: "";
flex: 1;
border-bottom: 1px solid #adb5bd;
}
.separator:not(:empty)::before {
margin-right: 0.25em;
}
.separator:not(:empty)::after {
margin-left: 0.25em;
}
</style>

View file

@ -0,0 +1,48 @@
<template>
<div class="message-actions-menu">
<!-- Add your action buttons here (e.g., Delete, Copy, Quote) -->
<button @click="onDelete">Delete</button>
<button @click="onCopy">Copy</button>
<button @click="onQuote">Quote</button>
</div>
</template>
<script>
export default {
methods: {
onDelete() {
// Implement delete action
this.$emit("delete");
},
onCopy() {
// Implement copy action
this.$emit("copy");
},
onQuote() {
// Implement quote action
this.$emit("quote");
},
},
};
</script>
<style scoped>
/* Style the message actions menu as needed */
.message-actions-menu {
position: absolute;
top: 50%;
transform: translateY(-50%);
display: none; /* Initially hidden */
/* Add styling for buttons and menu */
}
/* Style individual action buttons */
.message-actions-menu button {
/* Add button styles here */
}
/* Style menu display on hover */
.card:hover .message-actions-menu {
display: block;
}
</style>

View file

@ -0,0 +1,96 @@
<template>
<div class="row justify-content-start mb-2">
<div :class="messageWidthClass">
<div class="card bg-light border-0 text-dark">
<div class="card-header" v-if="getFileContent['filesize'] !== 0">
<p class="card-text">
{{ getFileContent["filename"] }} |
{{ getFileContent["filesize"] }} Bytes |
{{ getFileContent["filetype"] }}
</p>
</div>
<div class="card-body">
<p class="card-text">{{ message.msg }}</p>
</div>
<div class="card-footer p-0 bg-light border-top-0">
<p class="text-muted p-0 m-0 me-1 text-end">{{ getDateTime }}</p>
<!-- Display formatted timestamp in card-footer -->
</div>
<span
class="position-absolute top-0 start-100 translate-middle badge rounded-1 bg-secondary border border-white"
>
{{ message.broadcast_sender }}
</span>
</div>
</div>
<!-- Delete button outside of the card -->
<div class="col-auto">
<button class="btn btn-outline-secondary border-0" @click="deleteMessage">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</template>
<script>
import { deleteMessageFromDB } from "../js/chatHandler";
export default {
props: {
message: Object,
},
computed: {
getFileContent() {
try {
var filename = Object.keys(this.message._attachments)[0];
var filesize = this.message._attachments[filename]["length"];
var filetype = filename.split(".")[1];
// ensure filesize is 0 for hiding message header if no data is available
if (
typeof filename === "undefined" ||
filename === "" ||
filename === "-" ||
filename === "null"
) {
filesize = 0;
}
return { filename: filename, filesize: filesize, filetype: filetype };
} catch (e) {
console.log("file not loaded from database - empty?");
// we are only checking against filesize for displaying attachments
return { filesize: 0 };
}
},
messageWidthClass() {
// Calculate a Bootstrap grid class based on message length
// Adjust the logic as needed to fit your requirements
if (this.message.msg.length <= 50) {
return "col-4";
} else if (this.message.msg.length <= 100) {
return "col-6";
} else {
return "col-9";
}
},
deleteMessage() {
deleteMessageFromDB(this.message._id);
},
getDateTime() {
var datetime = new Date(this.message.timestamp * 1000).toLocaleString(
navigator.language,
{
hour: "2-digit",
minute: "2-digit",
},
);
return datetime;
},
},
};
</script>

View file

@ -0,0 +1,113 @@
<template>
<div class="row justify-content-end mb-2">
<!-- control area -->
<div class="col-auto p-0 m-0">
<button
class="btn btn-outline-secondary border-0 me-1"
@click="repeatMessage"
>
<i class="bi bi-arrow-repeat"></i>
</button>
<button class="btn btn-outline-secondary border-0" @click="deleteMessage">
<i class="bi bi-trash"></i>
</button>
</div>
<!-- message area -->
<div :class="messageWidthClass">
<div class="card bg-primary text-white">
<div class="card-header" v-if="getFileContent['filesize'] !== 0">
<p class="card-text">
{{ getFileContent["filename"] }} |
{{ getFileContent["filesize"] }} Bytes |
{{ getFileContent["filetype"] }}
</p>
</div>
<div class="card-body">
<p class="card-text">{{ message.msg }}</p>
</div>
<div class="card-footer p-0 bg-primary border-top-0">
<p class="text p-0 m-0 me-1 text-end">{{ getDateTime }}</p>
<!-- Display formatted timestamp in card-footer -->
</div>
<div class="card-footer p-0 border-top-0" v-if="message.percent < 100">
<div class="progress bg-secondary" :style="{ height: '10px' }">
<div
class="progress-bar progress-bar-striped overflow-visible"
role="progressbar"
:style="{ width: message.percent + '%', height: '10px' }"
:aria-valuenow="message.percent"
aria-valuemin="0"
aria-valuemax="100"
>
{{ message.percent }} % with {{ message.bytesperminute }} bpm
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import {
repeatMessageTransmission,
deleteMessageFromDB,
} from "../js/chatHandler";
export default {
props: {
message: Object,
},
computed: {
getFileContent() {
var filename = Object.keys(this.message._attachments)[0];
var filesize = this.message._attachments[filename]["length"];
var filetype = filename.split(".")[1];
// ensure filesize is 0 for hiding message header if no data is available
if (
typeof filename === "undefined" ||
filename === "" ||
filename === "-" ||
filename === "null"
) {
filesize = 0;
}
return { filename: filename, filesize: filesize, filetype: filetype };
},
messageWidthClass() {
// Calculate a Bootstrap grid class based on message length
// Adjust the logic as needed to fit your requirements
if (this.message.msg.length <= 50) {
return "col-4";
} else if (this.message.msg.length <= 100) {
return "col-6";
} else {
return "col-9";
}
},
repeatMessage() {
repeatMessageTransmission(this.message._id);
},
deleteMessage() {
deleteMessageFromDB(this.message._id);
},
getDateTime() {
var datetime = new Date(this.message.timestamp * 1000).toLocaleString(
navigator.language,
{
hour: "2-digit",
minute: "2-digit",
},
);
return datetime;
},
},
};
</script>

View file

@ -0,0 +1,135 @@
<template>
<div class="row justify-content-start mb-2">
<div :class="messageWidthClass">
<div class="card bg-light border-0 text-dark">
<div class="card-header" v-if="getFileContent['filesize'] !== 0">
<p class="card-text">
{{ getFileContent["filename"] }} |
{{ getFileContent["filesize"] }} Bytes |
{{ getFileContent["filetype"] }}
</p>
</div>
<div class="card-body">
<p class="card-text">{{ message.msg }}</p>
</div>
<div class="card-footer p-0 bg-light border-top-0">
<p class="text-muted p-0 m-0 me-1 text-end">{{ getDateTime }}</p>
<!-- Display formatted timestamp in card-footer -->
</div>
</div>
</div>
<!-- Delete button outside of the card -->
<div class="col-auto">
<button
class="btn btn-outline-secondary border-0 me-1"
@click="showMessageInfo"
data-bs-target="#messageInfoModal"
data-bs-toggle="modal"
>
<i class="bi bi-info-circle"></i>
</button>
<button
v-if="getFileContent['filesize'] !== 0"
class="btn btn-outline-secondary border-0 me-1"
@click="downloadAttachment"
>
<i class="bi bi-download"></i>
</button>
<button class="btn btn-outline-secondary border-0" @click="deleteMessage">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</template>
<script>
import {
deleteMessageFromDB,
requestMessageInfo,
getMessageAttachment,
} from "../js/chatHandler";
import { atob_FD } from "../js/freedata";
// pinia store setup
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { saveAs } from "file-saver";
import { useChatStore } from "../store/chatStore.js";
const chat = useChatStore(pinia);
export default {
props: {
message: Object,
},
methods: {
showMessageInfo() {
requestMessageInfo(this.message._id);
//let infoModal = Modal.getOrCreateInstance(document.getElementById('messageInfoModal'))
//console.log(this.infoModal)
//this.infoModal.show()
},
deleteMessage() {
deleteMessageFromDB(this.message._id);
},
async downloadAttachment() {
try {
// reset file store
chat.downloadFileFromDB = [];
const attachment = await getMessageAttachment(this.message._id);
const blob = new Blob([atob_FD(attachment[2])], {
type: `${attachment[1]};charset=utf-8`,
});
window.focus();
saveAs(blob, attachment[0]);
} catch (error) {
console.error("Failed to download attachment:", error);
}
},
},
computed: {
getFileContent() {
try {
var filename = Object.keys(this.message._attachments)[0];
var filesize = this.message._attachments[filename]["length"];
var filetype = filename.split(".")[1];
return { filename: filename, filesize: filesize, filetype: filetype };
} catch (e) {
console.log("file not loaded from database - empty?");
// we are only checking against filesize for displaying attachments
return { filesize: 0 };
}
},
messageWidthClass() {
// Calculate a Bootstrap grid class based on message length
// Adjust the logic as needed to fit your requirements
if (this.message.msg.length <= 50) {
return "col-4";
} else if (this.message.msg.length <= 100) {
return "col-6";
} else {
return "col-9";
}
},
getDateTime() {
var datetime = new Date(this.message.timestamp * 1000).toLocaleString(
navigator.language,
{
hour: "2-digit",
minute: "2-digit",
},
);
return datetime;
},
},
};
</script>

View file

@ -0,0 +1,171 @@
<template>
<div class="row justify-content-end mb-2">
<!-- control area -->
<div class="col-auto p-0 m-0">
<button
v-if="getFileContent['filesize'] !== 0"
class="btn btn-outline-secondary border-0 me-1"
@click="downloadAttachment"
>
<i class="bi bi-download"></i>
</button>
<button
class="btn btn-outline-secondary border-0 me-1"
@click="repeatMessage"
>
<i class="bi bi-arrow-repeat"></i>
</button>
<button
class="btn btn-outline-secondary border-0 me-1"
@click="showMessageInfo"
data-bs-target="#messageInfoModal"
data-bs-toggle="modal"
>
<i class="bi bi-info-circle"></i>
</button>
<button class="btn btn-outline-secondary border-0" @click="deleteMessage">
<i class="bi bi-trash"></i>
</button>
</div>
<!-- message area -->
<div :class="messageWidthClass">
<div class="card bg-primary text-white">
<div class="card-header" v-if="getFileContent['filesize'] !== 0">
<p class="card-text">
{{ getFileContent["filename"] }} |
{{ getFileContent["filesize"] }} Bytes |
{{ getFileContent["filetype"] }}
</p>
</div>
<div class="card-body">
<p class="card-text">{{ message.msg }}</p>
</div>
<div class="card-footer p-0 bg-primary border-top-0">
<p class="text p-0 m-0 me-1 text-end">{{ getDateTime }}</p>
<!-- Display formatted timestamp in card-footer -->
</div>
<div class="card-footer p-0 border-top-0" v-if="message.percent < 100">
<div
class="progress bg-secondary rounded-0 rounded-bottom"
:style="{ height: '10px' }"
>
<div
class="progress-bar progress-bar-striped overflow-visible"
role="progressbar"
:style="{ width: message.percent + '%', height: '10px' }"
:aria-valuenow="message.percent"
aria-valuemin="0"
aria-valuemax="100"
>
{{ message.percent }} % with {{ message.bytesperminute }} bpm
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { atob_FD } from "../js/freedata";
import {
repeatMessageTransmission,
deleteMessageFromDB,
requestMessageInfo,
getMessageAttachment,
} from "../js/chatHandler";
// pinia store setup
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { saveAs } from "file-saver";
import { useChatStore } from "../store/chatStore.js";
const chat = useChatStore(pinia);
export default {
props: {
message: Object,
},
methods: {
repeatMessage() {
repeatMessageTransmission(this.message._id);
},
deleteMessage() {
deleteMessageFromDB(this.message._id);
},
showMessageInfo() {
console.log("requesting message info.....");
requestMessageInfo(this.message._id);
//let infoModal = Modal.getOrCreateInstance(document.getElementById('messageInfoModal'))
//console.log(this.infoModal)
//this.infoModal.show()
},
async downloadAttachment() {
try {
// reset file store
chat.downloadFileFromDB = [];
const attachment = await getMessageAttachment(this.message._id);
const blob = new Blob([atob_FD(attachment[2])], {
type: `${attachment[1]};charset=utf-8`,
});
saveAs(blob, attachment[0]);
} catch (error) {
console.error("Failed to download attachment:", error);
}
},
},
computed: {
getFileContent() {
var filename = Object.keys(this.message._attachments)[0];
var filesize = this.message._attachments[filename]["length"];
var filetype = filename.split(".")[1];
// ensure filesize is 0 for hiding message header if no data is available
if (
typeof filename === "undefined" ||
filename === "" ||
filename === "-"
) {
filesize = 0;
}
return { filename: filename, filesize: filesize, filetype: filetype };
},
messageWidthClass() {
// Calculate a Bootstrap grid class based on message length
// Adjust the logic as needed to fit your requirements
if (this.message.msg.length <= 50) {
return "col-4";
} else if (this.message.msg.length <= 100) {
return "col-6";
} else {
return "col-9";
}
},
getDateTime() {
var datetime = new Date(this.message.timestamp * 1000).toLocaleString(
navigator.language,
{
hour: "2-digit",
minute: "2-digit",
},
);
return datetime;
},
},
};
</script>

View file

@ -0,0 +1,195 @@
<script setup lang="ts">
// @ts-nocheck
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
import { useChatStore } from "../store/chatStore.js";
const chat = useChatStore(pinia);
import { getRxBuffer } from "../js/sock.js";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
Title,
Tooltip,
Legend,
BarElement,
} from "chart.js";
import { Bar } from "vue-chartjs";
import { ref, computed } from "vue";
import annotationPlugin from "chartjs-plugin-annotation";
const newChatCall = ref(null);
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
Title,
Tooltip,
Legend,
BarElement,
annotationPlugin,
);
var beaconHistogramOptions = {
type: "bar",
bezierCurve: false, //remove curves from your plot
scaleShowLabels: false, //remove labels
tooltipEvents: [], //remove trigger from tooltips so they will'nt be show
pointDot: false, //remove the points markers
scaleShowGridLines: true, //set to false to remove the grids background
maintainAspectRatio: true,
plugins: {
legend: {
display: false,
},
annotation: {
annotations: [
{
type: "line",
mode: "horizontal",
scaleID: "y",
value: 0,
borderColor: "darkgrey", // Set the color to dark grey for the zero line
borderWidth: 0.5, // Set the line width
},
],
},
},
scales: {
x: {
position: "bottom",
display: false,
min: -10,
max: 15,
ticks: {
display: false,
},
},
y: {
display: false,
min: -5,
max: 10,
ticks: {
display: false,
},
},
},
};
//let dataArray = new Array(25).fill(0)
//dataArray = dataArray.add([-3, 10, 8, 5, 3, 0, -5])
//let dataArray1 = dataArray.shift(2)
//console.log(dataArray1)
//[-3, 10, 8, 5, 3, 0, -5]
try {
chat.beaconLabelArray = Object.values(
chat.sorted_beacon_list["DJ2LS-0"].timestamp,
);
chat.beaconDataArray = Object.values(chat.sorted_beacon_list["DJ2LS-0"].snr);
} catch (e) {
console.log(e);
}
const beaconHistogramData = computed(() => ({
labels: chat.beaconLabelArray,
datasets: [
{
data: chat.beaconDataArray,
tension: 0.1,
borderColor: "rgb(0, 255, 0)",
backgroundColor: function (context) {
var value = context.dataset.data[context.dataIndex];
return value >= 0 ? "green" : "red";
},
},
],
}));
function newChat() {
let callsign = this.newChatCall.value;
callsign = callsign.toUpperCase();
chat.callsign_list.add(callsign);
this.newChatCall.value = "";
}
function syncWithModem() {
getRxBuffer();
}
</script>
<template>
<nav class="navbar bg-body-tertiary border-bottom">
<div class="container">
<div class="row w-100">
<div class="col-3 p-0 me-2">
<div class="input-group bottom-0 m-0 ms-1">
<input
class="form-control"
maxlength="9"
style="text-transform: uppercase"
placeholder="callsign"
@keypress.enter="newChat()"
ref="newChatCall"
/>
<button
class="btn btn-sm btn-outline-success"
id="createNewChatButton"
type="button"
title="Start a new chat (enter dx call sign first)"
@click="newChat()"
>
new chat
<i class="bi bi-pencil-square" style="font-size: 1.2rem"></i>
</button>
</div>
</div>
<div class="col-5 ms-2 p-0">
<!-- right side of chat nav bar-->
<div class="input-group mb-0 p-0 w-50">
<button type="button" class="btn btn-outline-secondary" disabled>
Beacons
</button>
<div
class="form-floating border border-secondary-subtle border-1 rounded-end"
>
<Bar
:data="beaconHistogramData"
:options="beaconHistogramOptions"
width="300"
height="50"
/>
</div>
</div>
</div>
<div class="col-2 ms-2 p-0">
<div class="input-group mb-0 p-0">
<button
type="button"
class="btn btn-outline-secondary"
@click="syncWithModem()"
>
Modem Sync {{ state.rx_buffer_length }}
</button>
</div>
</div>
</div>
</div>
</nav>
</template>

View file

@ -0,0 +1,306 @@
<script setup lang="ts">
// @ts-nocheck
import {saveSettingsToFile} from '../js/settingsHandler';
import { setActivePinia } from 'pinia';
import pinia from '../store/index';
setActivePinia(pinia);
import { useSettingsStore } from '../store/settingsStore.js';
const settings = useSettingsStore(pinia);
import { useStateStore } from '../store/stateStore.js';
const state = useStateStore(pinia);
import { useChatStore } from '../store/chatStore.js';
const chat = useChatStore(pinia);
import chat_navbar from './chat_navbar.vue'
import chat_conversations from './chat_conversations.vue'
import chat_messages from './chat_messages.vue'
import {updateAllChat, newMessage, newBroadcast} from '../js/chatHandler'
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
} from 'chart.js'
import { Line } from 'vue-chartjs'
import { ref, computed } from 'vue';
import { VuemojiPicker, EmojiClickEventDetail } from 'vuemoji-picker'
const handleEmojiClick = (detail: EmojiClickEventDetail) => {
chat.inputText += detail.unicode
}
const chatModuleMessage=ref(null);
function transmitNewMessage(){
chat.inputText = chat.inputText.trim();
if (chat.inputText.length==0)
return;
if (chat.selectedCallsign.startsWith("BC-")) {
newBroadcast(chat.selectedCallsign, chat.inputText)
} else {
newMessage(chat.selectedCallsign, chat.inputText, chat.inputFile, chat.inputFileName, chat.inputFileSize, chat.inputFileType)
}
// finally do a cleanup
//chatModuleMessage.reset();
chat.inputText = '';
chatModuleMessage.value="";
// @ts-expect-error
resetFile()
}
function resetFile(event){
chat.inputFileName = '-'
chat.inputFileSize = '-'
chat.inputFileType = '-'
}
function readFile(event) {
const reader = new FileReader();
reader.onload = () => {
console.log(reader.result);
chat.inputFileName = event.target.files[0].name
chat.inputFileSize = event.target.files[0].size
chat.inputFileType = event.target.files[0].type
chat.inputFile = reader.result
calculateTimeNeeded()
// String.fromCharCode.apply(null, Array.from(chatFile))
};
reader.readAsArrayBuffer(event.target.files[0]);
}
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
)
// calculate time needed for transmitting a file
function calculateTimeNeeded(){
var calculatedSpeedPerMinutePER0 = []
var calculatedSpeedPerMinutePER25 = []
var calculatedSpeedPerMinutePER75 = []
// bpm vs snr with PER == 0
var snrList = [
{snr: -10, bpm: 100},
{snr: -5, bpm: 300},
{snr: 0, bpm: 800},
{snr: 5, bpm: 2500},
{snr: 10, bpm: 5300}
];
for (let i = 0; i < snrList.length; i++) {
var result = snrList.find(obj => {
return obj.snr === snrList[i].snr
})
calculatedSpeedPerMinutePER0.push(chat.inputFileSize / result.bpm)
calculatedSpeedPerMinutePER25.push(chat.inputFileSize / (result.bpm * 0.75))
calculatedSpeedPerMinutePER75.push(chat.inputFileSize / (result.bpm * 0.25))
}
chat.chartSpeedPER0 = calculatedSpeedPerMinutePER0
chat.chartSpeedPER25 = calculatedSpeedPerMinutePER25
chat.chartSpeedPER75 = calculatedSpeedPerMinutePER75
}
const speedChartData = computed(() => ({
labels: ['-10', '-5', '0', '5', '10'],
datasets: [
{ data: chat.chartSpeedPER0, label: 'PER 0%' ,tension: 0.1, borderColor: 'rgb(0, 255, 0)' },
{ data: chat.chartSpeedPER25, label: 'PER 25%' ,tension: 0.1, borderColor: 'rgb(255, 255, 0)'},
{ data: chat.chartSpeedPER75, label: 'PER 75%' ,tension: 0.1, borderColor: 'rgb(255, 0, 0)' }
]
}
));
</script>
<template>
<div class="container-fluid mt-2 p-0">
<input
type="checkbox"
id="expand_textarea"
class="btn-check"
autocomplete="off"
/>
<label
class="btn d-flex justify-content-center"
id="expand_textarea_label"
for="expand_textarea"
><i
id="expand_textarea_button"
class="bi bi-chevron-compact-up"
></i
></label>
<div class="input-group bottom-0 ms-2">
<button type="button" class="btn btn-outline-secondary border-0 rounded-pill me-1"
data-bs-toggle="modal" data-bs-target="#emojiPickerModal"
data-bs-backdrop="false"
>
<i
id="emojipickerbutton"
class="bi bi-emoji-smile p-0"
style="font-size: 1rem"
></i>
</button>
<!-- trigger file selection modal -->
<button type="button" class="btn btn-outline-secondary border-0 rounded-pill me-1" data-bs-toggle="modal" data-bs-target="#fileSelectionModal">
<i class="bi bi-paperclip" style="font-size: 1.2rem"></i>
</button>
<textarea
class="form-control border rounded-pill"
rows="1"
ref="chatModuleMessage"
placeholder="Message - Send with [Enter]"
v-model="chat.inputText"
@keyup.enter.exact="transmitNewMessage()"
></textarea>
<button
class="btn btn-sm btn-secondary ms-1 me-2 rounded-pill"
@click="transmitNewMessage()"
type="button"
>
<i
class="bi bi-send ms-4 me-4"
style="font-size: 1.2rem"
></i>
</button>
</div>
</div>
<!-- select file modal -->
<div
class="modal fade"
id="fileSelectionModal"
tabindex="-1"
aria-labelledby="fileSelectionModalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="staticBackdropLabel">File Attachment</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" @click="resetFile"></button>
</div>
<div class="modal-body">
<div class="alert alert-warning d-flex align-items-center" role="alert">
<i class="bi bi-exclamation-triangle-fill ms-2 me-2"></i>
<div>
Transmission speed over HF channels is very limited!
</div>
</div>
<div class="input-group-text mb-3">
<input class="" type="file" ref="doc" @change="readFile" />
</div>
<div class="btn-group me-2" role="group" aria-label="Basic outlined example">
<button type="button" class="btn btn-secondary">Type</button>
<button type="button" class="btn btn-secondary disabled">{{chat.inputFileType}}</button>
</div>
<div class="btn-group me-2" role="group" aria-label="Basic outlined example">
<button type="button" class="btn btn-secondary">Size</button>
<button type="button" class="btn btn-secondary disabled">{{chat.inputFileSize}}</button>
</div>
<Line :data="speedChartData" />
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" @click="resetFile">Reset</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Append</button>
</div>
</div>
</div>
</div>
<!-- Emoji Picker Modal -->
<div class="modal fade" id="emojiPickerModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content">
<div class="modal-body p-0">
<VuemojiPicker @emojiClick="handleEmojiClick" />
</div>
</div>
</div>
</div>
</template>

570
gui/src/components/main.vue Normal file
View file

@ -0,0 +1,570 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
import main_modals from "./main_modals.vue";
import main_top_navbar from "./main_top_navbar.vue";
import main_audio from "./main_audio.vue";
import main_rig_control from "./main_rig_control.vue";
import main_my_station from "./main_my_station.vue";
import main_updater from "./main_updater.vue";
import settings_view from "./settings.vue";
import main_active_rig_control from "./main_active_rig_control.vue";
import main_footer_navbar from "./main_footer_navbar.vue";
import main_active_stats from "./main_active_stats.vue";
import main_active_broadcasts from "./main_active_broadcasts.vue";
import main_active_heard_stations from "./main_active_heard_stations.vue";
import main_active_audio_level from "./main_active_audio_level.vue";
import chat from "./chat.vue";
import { stopTransmission } from "../js/sock.js";
const version = import.meta.env.PACKAGE_VERSION;
function stopAllTransmissions() {
console.log("stopping transmissions");
stopTransmission();
}
function openWebExternal(url) {
open(url);
}
</script>
<template>
<!-------------------------------- 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"
id="mainToastContainer"
></div>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-sm-auto bg-body-secondary border-end">
<div
class="d-flex flex-sm-column flex-row flex-nowrap align-items-center sticky-top"
>
<div
class="list-group bg-body-secondary"
id="main-list-tab"
role="tablist"
style="margin-top: 100px"
>
<a
class="list-group-item list-group-item-action active"
id="list-modem-list"
data-bs-toggle="list"
href="#list-modem"
role="tab"
aria-controls="list-modem"
title="Home"
><i class="bi bi-house-door-fill h3"></i
></a>
<a
class="list-group-item list-group-item-action"
id="list-chat-list"
data-bs-toggle="list"
href="#list-chat"
role="tab"
aria-controls="list-chat"
title="Chat"
><i class="bi bi-chat-text h3"></i
></a>
<a
class="list-group-item list-group-item-action d-none"
id="list-mesh-list"
data-bs-toggle="list"
href="#list-mesh"
role="tab"
aria-controls="list-mesh"
><i class="bi bi-rocket h3"></i
></a>
<a
class="list-group-item list-group-item-action mt-2 border"
id="list-info-list"
data-bs-toggle="list"
href="#list-info"
role="tab"
aria-controls="list-info"
title="About"
><i class="bi bi-info h3"></i
></a>
<a
class="list-group-item list-group-item-action d-none"
id="list-logger-list"
data-bs-toggle="list"
href="#list-logger"
role="tab"
aria-controls="list-logger"
><i class="bi bi-activity h3"></i
></a>
<a
class="list-group-item list-group-item-action rounded-bottom"
id="list-settings-list"
data-bs-toggle="list"
href="#list-settings"
role="tab"
aria-controls="list-settings"
title="Settings"
><i class="bi bi-gear-wide-connected h3"></i
></a>
<a
class="btn border btn-outline-danger list-group-item mt-5"
id="stop_transmission_connection"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="false"
@click="stopAllTransmissions()"
title="Abort session and stop transmissions"
><i class="bi bi-sign-stop-fill h3"></i
></a>
</div>
</div>
</div>
<div class="col-sm min-vh-100 m-0 p-0">
<!-- content -->
<div class="tab-content" id="nav-tabContent-settings">
<div
class="tab-pane fade show active"
id="list-modem"
role="tabpanel"
aria-labelledby="list-modem-list"
>
<!-- TOP NAVBAR -->
<main_top_navbar />
<div
id="blurdiv"
style="
-webkit-filter: blur(0px);
filter: blur(0px);
height: 100vh;
"
>
<!--beginn of blur div -->
<!-------------------------------- MAIN AREA ---------------->
<!------------------------------------------------------------------------------------------>
<div class="container p-3">
<div
class="row collapse multi-collapse show mt-4"
id="collapseFirstRow"
>
<div class="col">
<main_audio />
</div>
<div class="col">
<main_rig_control />
</div>
</div>
<div
class="row collapse multi-collapse show mt-4"
id="collapseSecondRow"
>
<div class="col">
<main_my_station />
</div>
<div class="col">
<main_updater />
</div>
</div>
</div>
<div class="container">
<div class="row collapse multi-collapse" id="collapseThirdRow">
<main_active_rig_control />
<div class="col-5">
<main_active_audio_level />
</div>
<div class="col">
<main_active_broadcasts />
</div>
</div>
<div
class="row collapse multi-collapse mt-3"
id="collapseFourthRow"
>
<div class="col-5">
<main_active_stats />
</div>
<div class="col">
<main_active_heard_stations />
</div>
</div>
</div>
</div>
</div>
<div
class="tab-pane fade d-none"
id="list-mesh"
role="tabpanel"
aria-labelledby="list-mesh-list"
>
<div class="container">
<nav>
<div class="nav nav-tabs" id="nav-tab-mesh" role="tablist-mesh">
<button
class="nav-link active"
id="nav-route-tab"
data-bs-toggle="tab"
data-bs-target="#nav-route"
type="button"
role="tab"
aria-controls="nav-route"
aria-selected="true"
>
Routes
</button>
<button
class="nav-link"
id="nav-signaling-tab"
data-bs-toggle="tab"
data-bs-target="#nav-signaling"
type="button"
role="tab"
aria-controls="nav-signaling"
aria-selected="false"
>
Signaling
</button>
<button
class="nav-link"
id="nav-actions-tab"
data-bs-toggle="tab"
data-bs-target="#nav-actions"
type="button"
role="tab"
aria-controls="nav-actions"
aria-selected="false"
>
Actions
</button>
</div>
</nav>
<div class="tab-content d-none" id="nav-tabContent-Mesh">
<div
class="tab-pane fade show active vw-100 vh-90 overflow-auto"
id="nav-route"
role="tabpanel"
aria-labelledby="nav-route-tab"
>
<div class="container-fluid">
<div
class="table-responsive overflow-auto"
style="max-width: 99vw; max-height: 99vh"
>
<table class="table table-hover table-sm">
<thead>
<tr>
<th scope="col">Timestamp</th>
<th scope="col">DXCall</th>
<th scope="col">Router</th>
<th scope="col">Hops</th>
<th scope="col">Score</th>
<th scope="col">SNR</th>
</tr>
</thead>
<tbody id="mesh-table"></tbody>
</table>
</div>
</div>
</div>
<div
class="tab-pane fade"
id="nav-signaling"
role="tabpanel"
aria-labelledby="nav-signaling-tab"
>
<div class="container-fluid">
<div
class="table-responsive overflow-auto"
style="max-width: 99vw; max-height: 99vh"
>
<table class="table table-hover table-sm">
<thead>
<tr>
<th scope="col">Timestamp</th>
<th scope="col">Destination</th>
<th scope="col">Origin</th>
<th scope="col">Frametype</th>
<th scope="col">Payload</th>
<th scope="col">Attempt</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody id="mesh-signalling-table"></tbody>
</table>
</div>
</div>
</div>
<div
class="tab-pane fade"
id="nav-actions"
role="tabpanel-mesh"
aria-labelledby="nav-actions-tab"
>
<div class="input-group mt-1">
<input
type="text"
class="form-control"
style="max-width: 6rem; text-transform: uppercase"
placeholder="DXcall"
pattern="[A-Z]*"
id="dxCallMesh"
maxlength="11"
aria-label="Input group"
aria-describedby="btnGroupAddon"
/>
<button
id="transmit_mesh_ping"
type="button"
class="btn btn-primary"
>
mesh ping
</button>
</div>
</div>
</div>
</div>
</div>
<div
class="tab-pane fade"
id="list-info"
role="tabpanel"
aria-labelledby="list-info-list"
>
<h1 class="modal-title fs-5" id="aboutModalLabel">
FreeDATA - {{ version }}
</h1>
<h4 class="fs-5">modem version - {{ state.modem_version }}</h4>
<div class="container-fluid">
<div class="row mt-2">
<div
class="btn-toolbar mx-auto"
role="toolbar"
aria-label="Toolbar with button groups"
>
<div class="btn-group">
<button
class="btn btn-sm bi bi-geo-alt btn-secondary me-2"
id="openExplorer"
type="button"
data-bs-placement="bottom"
@click="openWebExternal('https://explorer.freedata.app')"
>
Explorer map
</button>
</div>
<div class="btn-group">
<button
class="btn btn-sm btn-secondary me-2 bi bi-graph-up"
id="btnStats"
type="button"
data-bs-placement="bottom"
@click="
openWebExternal('https://statistics.freedata.app/')
"
>
Statistics
</button>
</div>
<div class="btn-group">
<button
class="btn btn-secondary bi bi-bookmarks me-2"
id="fdWww"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
title="FreeDATA website"
role="button"
@click="openWebExternal('https://freedata.app')"
>
Website
</button>
</div>
<div class="btn-group">
<button
class="btn btn-secondary bi bi-github me-2"
id="ghUrl"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
title="Github"
role="button"
@click="
openWebExternal('https://github.com/dj2ls/freedata')
"
>
Github
</button>
</div>
<div class="btn-group">
<button
class="btn btn-secondary bi bi-wikipedia me-2"
id="wikiUrl"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
title="Wiki"
role="button"
@click="openWebExternal('https://wiki.freedata.app')"
>
Wiki
</button>
</div>
<div class="btn-group">
<button
class="btn btn-secondary bi bi-discord"
id="discordUrl"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
title="Discord"
role="button"
@click="openWebExternal('https://discord.freedata.app')"
>
Discord
</button>
</div>
</div>
</div>
<div class="row mt-5">
<h6>Special thanks to</h6>
<hr />
</div>
<div class="row">
<div class="col-4" id="divContrib"></div>
<div class="col-4" id="divContrib2"></div>
<div class="col-4" id="divContrib3"></div>
</div>
</div>
</div>
<div
class="tab-pane fade"
id="list-chat"
role="tabpanel"
aria-labelledby="list-chat-list"
>
<chat />
</div>
<div
class="tab-pane fade"
id="list-logger"
role="tabpanel"
aria-labelledby="list-logger-list"
>
<div class="container">
<nav
class="navbar fixed-top bg-body-tertiary border-bottom"
style="margin-left: 87px"
>
<div class="container-fluid">
<input
type="checkbox"
class="btn-check"
id="enable_filter_info"
autocomplete="off"
checked
/>
<label class="btn btn-outline-info" for="enable_filter_info"
>info</label
>
<input
type="checkbox"
class="btn-check"
id="enable_filter_debug"
autocomplete="off"
/>
<label
class="btn btn-outline-primary"
for="enable_filter_debug"
>debug</label
>
<input
type="checkbox"
class="btn-check"
id="enable_filter_warning"
autocomplete="off"
/>
<label
class="btn btn-outline-warning"
for="enable_filter_warning"
>warning</label
>
<input
type="checkbox"
class="btn-check"
id="enable_filter_error"
autocomplete="off"
/>
<label
class="btn btn-outline-danger"
for="enable_filter_error"
>error</label
>
</div>
</nav>
<div class="container-fluid mt-5">
<div class="tableFixHead">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Timestamp</th>
<th scope="col">Type</th>
<th scope="col">Area</th>
<th scope="col">Log entry</th>
</tr>
</thead>
<tbody id="log">
<!--
<tr>
<th scope="row">1</th>
<td>Mark</td>
<td>Otto</td>
<td>@mdo</td>
</tr>
-->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<settings_view />
<!---------------------------------------------------------------------- FOOTER AREA ------------------------------------------------------------>
<div class="container">
<main_footer_navbar />
</div>
</div>
</div>
</div>
<main_modals />
</template>

View file

@ -0,0 +1,188 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
import { record_audio } from "../js/sock.js";
function startStopRecordAudio() {
record_audio();
}
</script>
<template>
<div class="card mb-1">
<div class="card-header p-1">
<div class="container">
<div class="row">
<div class="col-1">
<i class="bi bi-volume-up" style="font-size: 1.2rem"></i>
</div>
<div class="col-3">
<strong class="fs-5">Audio</strong>
</div>
<div class="col-7">
<button
type="button"
id="audioModalButton"
data-bs-toggle="modal"
data-bs-target="#audioModal"
class="btn btn-sm btn-outline-secondary me-1"
>
Tune
</button>
<button
type="button"
id="startStopRecording"
class="btn btn-sm"
@click="startStopRecordAudio()"
v-bind:class="{
'btn-outline-secondary': state.audio_recording === 'False',
'btn-secondary': state.audio_recording === 'True',
}"
>
Record
</button>
</div>
<div class="col-1 text-end">
<button
type="button"
id="openHelpModalAudioLevel"
data-bs-toggle="modal"
data-bs-target="#audioLevelHelpModal"
class="btn m-0 p-0 border-0"
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body p-2">
<div class="container">
<div class="row">
<div class="col-sm">
<div
class="progress mb-0 rounded-0 rounded-top"
style="height: 22px"
>
<div
class="progress-bar progress-bar-striped bg-primary force-gpu"
id="noise_level"
role="progressbar"
:style="{ width: state.s_meter_strength_percent + '%' }"
aria-valuenow="{{state.s_meter_strength_percent}}"
aria-valuemin="0"
aria-valuemax="100"
></div>
<p
class="justify-content-center d-flex position-absolute w-100"
id="noise_level_value"
>
S-Meter(dB): {{ state.s_meter_strength_raw }}
</p>
</div>
<div
class="progress mb-0 rounded-0 rounded-bottom"
style="height: 8px"
>
<div
class="progress-bar progress-bar-striped bg-warning"
role="progressbar"
style="width: 1%"
aria-valuenow="1"
aria-valuemin="0"
aria-valuemax="100"
></div>
<div
class="progress-bar bg-success"
role="progressbar"
style="width: 89%"
aria-valuenow="50"
aria-valuemin="0"
aria-valuemax="100"
></div>
<div
class="progress-bar progress-bar-striped bg-warning"
role="progressbar"
style="width: 20%"
aria-valuenow="20"
aria-valuemin="0"
aria-valuemax="100"
></div>
<div
class="progress-bar progress-bar-striped bg-danger"
role="progressbar"
style="width: 29%"
aria-valuenow="29"
aria-valuemin="0"
aria-valuemax="100"
></div>
</div>
</div>
<div class="col-sm">
<div
class="progress mb-0 rounded-0 rounded-top"
style="height: 22px"
>
<div
class="progress-bar progress-bar-striped bg-primary force-gpu"
id="dbfs_level"
role="progressbar"
:style="{ width: state.dbfs_level_percent + '%' }"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
></div>
<p
class="justify-content-center d-flex position-absolute w-100"
id="dbfs_level_value"
>
{{ state.dbfs_level }} dBFS
</p>
</div>
<div
class="progress mb-0 rounded-0 rounded-bottom"
style="height: 8px"
>
<div
class="progress-bar progress-bar-striped bg-warning"
role="progressbar"
style="width: 1%"
aria-valuenow="1"
aria-valuemin="0"
aria-valuemax="100"
></div>
<div
class="progress-bar bg-success"
role="progressbar"
style="width: 89%"
aria-valuenow="50"
aria-valuemin="0"
aria-valuemax="100"
></div>
<div
class="progress-bar progress-bar-striped bg-warning"
role="progressbar"
style="width: 20%"
aria-valuenow="20"
aria-valuemin="0"
aria-valuemax="100"
></div>
<div
class="progress-bar progress-bar-striped bg-danger"
role="progressbar"
style="width: 29%"
aria-valuenow="29"
aria-valuemin="0"
aria-valuemax="100"
></div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,121 @@
<script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
import { sendCQ, sendPing, startBeacon, stopBeacon } from "../js/sock.js";
function transmitCQ() {
sendCQ();
}
function transmitPing() {
sendPing((<HTMLInputElement>document.getElementById("dxCall")).value);
}
function startStopBeacon() {
switch (state.beacon_state) {
case "False":
startBeacon(settings.beacon_interval);
break;
case "True":
stopBeacon();
break;
default:
}
}
</script>
<template>
<div class="card mb-1">
<div class="card-header p-1">
<div class="container">
<div class="row">
<div class="col-1">
<i class="bi bi-broadcast" style="font-size: 1.2rem"></i>
</div>
<div class="col-10">
<strong class="fs-5">Broadcasts</strong>
</div>
<div class="col-1 text-end">
<button
type="button"
id="openHelpModalBroadcasts"
data-bs-toggle="modal"
data-bs-target="#broadcastsHelpModal"
class="btn m-0 p-0 border-0"
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body p-2">
<div class="row">
<div class="col-md-auto">
<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-outline-secondary ms-1"
id="sendPing"
type="button"
data-bs-placement="bottom"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="false"
title="Send a ping request to a remote station"
@click="transmitPing()"
>
Ping
</button>
<button
class="btn btn-sm btn-outline-secondary ms-1"
id="sendCQ"
type="button"
title="Send a CQ to the world"
@click="transmitCQ()"
>
Call CQ
</button>
<button
type="button"
id="startBeacon"
class="btn btn-sm ms-1"
@click="startStopBeacon()"
v-bind:class="{
'btn-success': state.beacon_state === 'True',
'btn-outline-secondary': state.beacon_state === 'False',
}"
title="Toggle beacon mode. The interval can be set in settings. While sending a beacon, you can receive ping requests and open a datachannel. If a datachannel is opened, the beacon pauses."
>
<i class="bi bi-soundwave"></i> Toggle beacon
</button>
</div>
</div>
</div>
<!-- end of row-->
</div>
</div>
</template>

View file

@ -0,0 +1,105 @@
<script setup lang="ts">
// @ts-nocheck
const { distance } = require("qth-locator");
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
function getDateTime(timestampRaw) {
var datetime = new Date(timestampRaw * 1000).toLocaleString(
navigator.language,
{
hourCycle: "h23",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
},
);
return datetime;
}
function getMaidenheadDistance(dxGrid) {
try {
return parseInt(distance(settings.mygrid, dxGrid));
} catch (e) {
//
}
}
</script>
<template>
<div class="card mb-1 h-100">
<!--325px-->
<div class="card-header p-1">
<div class="container">
<div class="row">
<div class="col-auto">
<i class="bi bi-list-columns-reverse" style="font-size: 1.2rem"></i>
</div>
<div class="col-10">
<strong class="fs-5">Heard stations</strong>
</div>
<div class="col-1 text-end">
<button
type="button"
id="openHelpModalHeardStations"
data-bs-toggle="modal"
data-bs-target="#heardStationsHelpModal"
class="btn m-0 p-0 border-0"
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body p-0" style="overflow-y: overlay">
<div class="table-responsive">
<!-- START OF TABLE FOR HEARD STATIONS -->
<table class="table table-sm" id="tblHeardStationList">
<thead>
<tr>
<th scope="col" id="thTime">
<i id="hslSort" class="bi bi-sort-up"></i>Time
</th>
<th scope="col" id="thFreq">Frequency</th>
<th>&nbsp;</th>
<th scope="col" id="thDxcall">DXCall</th>
<th scope="col" id="thDxgrid">DXGrid</th>
<th scope="col" id="thDist">Distance</th>
<th scope="col" id="thType">Type</th>
<th scope="col" id="thSnr">SNR (rx/dx)</th>
<!--<th scope="col">Off</th>-->
</tr>
</thead>
<tbody id="heardstations">
<!--https://vuejs.org/guide/essentials/list.html-->
<tr v-for="item in state.heard_stations" :key="item.timestamp">
<td>{{ getDateTime(item.timestamp) }}</td>
<td>{{ item.frequency }}</td>
<td>&nbsp;</td>
<td>
<span class="badge bg-secondary">{{ item.dxcallsign }}</span>
</td>
<td>{{ item.dxgrid }}</td>
<td>{{ getMaidenheadDistance(item.dxgrid) }} km</td>
<td>{{ item.datatype }}</td>
<td>{{ item.snr }}</td>
<!--<td>{{ item.offset }}</td>-->
</tr>
</tbody>
</table>
</div>
<!-- END OF HEARD STATIONS TABLE -->
</div>
</div>
</template>

View file

@ -0,0 +1,153 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
import { set_frequency, set_mode, set_rf_level } from "../js/sock.js";
function set_hamlib_frequency() {
set_frequency(state.new_frequency);
}
function set_hamlib_mode() {
set_mode(state.mode);
}
function set_hamlib_rf_level() {
set_rf_level(state.rf_level);
}
</script>
<template>
<div class="mb-3">
<div class="card mb-1">
<div class="card-header p-1">
<div class="container">
<div class="row">
<div class="col-1">
<i class="bi bi-house-door" style="font-size: 1.2rem"></i>
</div>
<div class="col-10">
<strong class="fs-5 me-2">Radio control</strong>
<span
class="badge"
v-bind:class="{
'text-bg-success': state.hamlib_status === 'connected',
'text-bg-danger disabled':
state.hamlib_status === 'disconnected',
}"
>{{ state.hamlib_status }}</span
>
</div>
<div class="col-1 text-end">
<button
type="button"
id="openHelpModalStation"
data-bs-toggle="modal"
data-bs-target="#stationHelpModal"
class="btn m-0 p-0 border-0"
disabled
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body p-2">
<div class="input-group bottom-0 m-0">
<div class="me-2">
<div class="input-group">
<span class="input-group-text">QRG</span>
<span class="input-group-text">{{ state.frequency }} Hz</span>
<span class="input-group-text">QSY</span>
<input
type="text"
class="form-control"
v-model="state.new_frequency"
style="max-width: 8rem"
pattern="[0-9]*"
list="frequencyDataList"
v-bind:class="{
disabled: state.hamlib_status === 'disconnected',
}"
/>
<datalist id="frequencyDataList">
<option selected value="7053000">40m | USB | EU, US</option>
<option value="14093000">20m | USB | EU, US</option>
<option value="21093000">15m | USB | EU, US</option>
<option value="24908000">12m | USB | EU, US</option>
<option value="28093000">10m | USB | EU, US</option>
<option value="50308000">6m | USB | US</option>
<option value="50616000">6m | USB | EU, US</option>
</datalist>
<button
class="btn btn-sm btn-outline-success"
type="button"
@click="set_hamlib_frequency"
v-bind:class="{
disabled: state.hamlib_status === 'disconnected',
}"
>
Apply
</button>
</div>
</div>
<div class="me-2">
<div class="input-group">
<span class="input-group-text">Mode</span>
<select
class="form-control"
v-model="state.mode"
@click="set_hamlib_mode()"
v-bind:class="{
disabled: state.hamlib_status === 'disconnected',
}"
>
<option value="USB">USB</option>
<option value="LSB">LSB</option>
<option value="PKTUSB">PKT-U</option>
<option value="PKTLSB">PKT-L</option>
<option value="AM">AM</option>
<option value="FM">FM</option>
<option value="PKTFM">PKTFM</option>
</select>
</div>
</div>
<div class="me-2">
<div class="input-group">
<span class="input-group-text">Power</span>
<select
class="form-control"
v-model="state.rf_level"
@click="set_hamlib_rf_level()"
v-bind:class="{
disabled: state.hamlib_status === 'disconnected',
}"
>
<option value="0">-</option>
<option value="10">10</option>
<option value="20">20</option>
<option value="30">30</option>
<option value="40">40</option>
<option value="50">50</option>
<option value="60">60</option>
<option value="70">70</option>
<option value="80">80</option>
<option value="90">90</option>
<option value="100">100</option>
</select>
<span class="input-group-text">%</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,396 @@
<script setup lang="ts">
// @ts-nocheck
// reason for no check is, that we have some mixing of typescript and chart js which seems to be not to be fixed that easy
import { saveSettingsToFile } from "../js/settingsHandler";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from "chart.js";
import { Line, Scatter } from "vue-chartjs";
import { computed } from "vue";
function selectStatsControl(obj) {
switch (obj.delegateTarget.id) {
case "list-waterfall-list":
settings.spectrum = "waterfall";
break;
case "list-scatter-list":
settings.spectrum = "scatter";
break;
case "list-chart-list":
settings.spectrum = "chart";
break;
default:
settings.spectrum = "waterfall";
}
saveSettingsToFile();
}
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
);
// https://www.chartjs.org/docs/latest/samples/line/segments.html
const skipped = (speedCtx, value) =>
speedCtx.p0.skip || speedCtx.p1.skip ? value : undefined;
const down = (speedCtx, value) =>
speedCtx.p0.parsed.y > speedCtx.p1.parsed.y ? value : undefined;
var transmissionSpeedChartOptions = {
//type: "line",
responsive: true,
animations: true,
maintainAspectRatio: false,
cubicInterpolationMode: "monotone",
tension: 0.4,
scales: {
SNR: {
type: "linear",
ticks: { beginAtZero: false, color: "rgb(255, 99, 132)" },
position: "right",
},
SPEED: {
type: "linear",
ticks: { beginAtZero: false, color: "rgb(120, 100, 120)" },
position: "left",
grid: {
drawOnChartArea: false, // only want the grid lines for one axis to show up
},
},
x: { ticks: { beginAtZero: true } },
},
};
const transmissionSpeedChartData = computed(() => ({
labels: state.arq_speed_list_timestamp,
datasets: [
{
type: "line",
label: "SNR[dB]",
data: state.arq_speed_list_snr,
borderColor: "rgb(75, 192, 192, 1.0)",
pointRadius: 1,
segment: {
borderColor: (speedCtx) =>
skipped(speedCtx, "rgb(0,0,0,0.4)") ||
down(speedCtx, "rgb(192,75,75)"),
borderDash: (speedCtx) => skipped(speedCtx, [3, 3]),
},
spanGaps: true,
backgroundColor: "rgba(75, 192, 192, 0.2)",
order: 1,
yAxisID: "SNR",
},
{
type: "bar",
label: "Speed[bpm]",
data: state.arq_speed_list_bpm,
borderColor: "rgb(120, 100, 120, 1.0)",
backgroundColor: "rgba(120, 100, 120, 0.2)",
order: 0,
yAxisID: "SPEED",
},
],
}));
const scatterChartOptions = {
responsive: true,
maintainAspectRatio: false,
scales: {
x: {
type: "linear",
position: "bottom",
grid: {
display: true,
lineWidth: 1, // Set the line width for x-axis grid lines
},
ticks: {
display: false,
},
},
y: {
type: "linear",
position: "left",
grid: {
display: true,
lineWidth: 1, // Set the line width for y-axis grid lines
},
ticks: {
display: false,
},
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
},
},
};
// dummy data
//state.scatter = [{"x":"166","y":"46"},{"x":"-193","y":"-139"},{"x":"-165","y":"-291"},{"x":"311","y":"-367"},{"x":"389","y":"199"},{"x":"78","y":"372"},{"x":"242","y":"-431"},{"x":"-271","y":"-248"},{"x":"28","y":"-130"},{"x":"-20","y":"187"},{"x":"74","y":"362"},{"x":"-316","y":"-229"},{"x":"-180","y":"261"},{"x":"321","y":"360"},{"x":"438","y":"-288"},{"x":"378","y":"-94"},{"x":"462","y":"-163"},{"x":"-265","y":"248"},{"x":"210","y":"314"},{"x":"230","y":"-320"},{"x":"261","y":"-244"},{"x":"-283","y":"-373"}]
const scatterChartData = computed(() => ({
datasets: [
{
type: "scatter",
fill: true,
data: state.scatter,
label: "Scatter",
tension: 0.1,
borderColor: "rgb(0, 255, 0)",
},
],
}));
</script>
<script lang="ts">
import { initWaterfall } from "../js/waterfallHandler.js";
export default {
mounted() {
// This code will be executed after the component is mounted to the DOM
// You can access DOM elements or perform other initialization here
//const myElement = this.$refs.waterfall; // Access the DOM element with ref
// init waterfall
initWaterfall();
},
};
</script>
<template>
<div class="card mb-1" style="height: calc(var(--variable-height) - 20px)">
<div class="card-header p-1">
<div class="container">
<div class="row">
<div class="col-11">
<div class="btn-group" role="group">
<div
class="list-group list-group-horizontal"
id="list-tab"
role="tablist"
>
<a
class="py-1 list-group-item list-group-item-action"
id="list-waterfall-list"
data-bs-toggle="list"
href="#list-waterfall"
role="tab"
aria-controls="list-waterfall"
v-bind:class="{ active: settings.spectrum === 'waterfall' }"
@click="selectStatsControl($event)"
><strong><i class="bi bi-water"></i></strong
></a>
<a
class="py-1 list-group-item list-group-item-action"
id="list-scatter-list"
data-bs-toggle="list"
href="#list-scatter"
role="tab"
aria-controls="list-scatter"
v-bind:class="{ active: settings.spectrum === 'scatter' }"
@click="selectStatsControl($event)"
><strong><i class="bi bi-border-outer"></i></strong
></a>
<a
class="py-1 list-group-item list-group-item-action"
id="list-chart-list"
data-bs-toggle="list"
href="#list-chart"
role="tab"
aria-controls="list-chart"
v-bind:class="{ active: settings.spectrum === 'chart' }"
@click="selectStatsControl($event)"
><strong><i class="bi bi-graph-up-arrow"></i></strong
></a>
</div>
</div>
<div class="btn-group" role="group" aria-label="Busy indicators">
<button
class="btn btn-sm ms-1 p-1 disabled"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
v-bind:class="{
'btn-warning': state.getChannelBusySlotState(0) === true,
'btn-outline-secondary':
state.getChannelBusySlotState(0) === false,
}"
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
>
S1
</button>
<button
class="btn btn-sm p-1 disabled"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
v-bind:class="{
'btn-warning': state.getChannelBusySlotState(1) === true,
'btn-outline-secondary':
state.getChannelBusySlotState(1) === false,
}"
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
>
S2
</button>
<button
class="btn btn-sm p-1 disabled"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
v-bind:class="{
'btn-warning': state.getChannelBusySlotState(2) === true,
'btn-outline-secondary':
state.getChannelBusySlotState(2) === false,
}"
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
>
S3
</button>
<button
class="btn btn-sm p-1 disabled"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
v-bind:class="{
'btn-warning': state.getChannelBusySlotState(3) === true,
'btn-outline-secondary':
state.getChannelBusySlotState(3) === false,
}"
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
>
S4
</button>
<button
class="btn btn-sm p-1 disabled"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
v-bind:class="{
'btn-warning': state.getChannelBusySlotState(4) === true,
'btn-outline-secondary':
state.getChannelBusySlotState(4) === false,
}"
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
>
S5
</button>
<button
class="btn btn-sm p-1 disabled"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
title="Recieving data: illuminates <strong class='text-success'>green</strong> if receiving codec2 data"
v-bind:class="{
'btn-success': state.is_codec2_traffic === 'True',
'btn-outline-secondary': state.is_codec2_traffic === 'False',
}"
>
data
</button>
</div>
</div>
<div class="col-1 text-end">
<button
type="button"
id="openHelpModalWaterfall"
data-bs-toggle="modal"
data-bs-target="#waterfallHelpModal"
class="btn m-0 p-0 border-0"
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body p-1">
<div class="tab-content" id="nav-stats-tabContent">
<div
class="tab-pane fade"
v-bind:class="{ 'show active': settings.spectrum === 'waterfall' }"
id="list-waterfall"
role="stats_tabpanel"
aria-labelledby="list-waterfall-list"
>
<canvas
ref="waterfall"
id="waterfall"
style="position: relative; z-index: 2"
class="force-gpu h-100 w-100"
></canvas>
</div>
<div
class="tab-pane fade"
v-bind:class="{ 'show active': settings.spectrum === 'scatter' }"
id="list-scatter"
role="tabpanel"
aria-labelledby="list-scatter-list"
>
<Scatter :data="scatterChartData" :options="scatterChartOptions" />
</div>
<div
class="tab-pane fade"
v-bind:class="{ 'show active': settings.spectrum === 'chart' }"
id="list-chart"
role="tabpanel"
aria-labelledby="list-chart-list"
>
<Line
:data="transmissionSpeedChartData"
:options="transmissionSpeedChartOptions"
/>
</div>
</div>
<!--278px-->
</div>
</div>
</template>

View file

@ -0,0 +1,61 @@
<script setup>
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useAudioStore } from "../store/audioStore.js";
const audio = useAudioStore(pinia);
</script>
<template>
<div class="card mb-0">
<div class="card-header p-1">
<div class="container">
<div class="row">
<div class="col-1">
<i class="bi bi-volume-up" style="font-size: 1.2rem"></i>
</div>
<div class="col-10">
<strong class="fs-5">Audio devices</strong>
</div>
<div class="col-1 text-end">
<button
type="button"
id="openHelpModalAudio"
data-bs-toggle="modal"
data-bs-target="#audioHelpModal"
class="btn m-0 p-0 border-0"
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body p-2" style="height: 100px">
<div class="input-group input-group-sm mb-1">
<span class="input-group-text">
<i class="bi bi-mic-fill" style="font-size: 1rem"></i>
</span>
<select
class="form-select form-select-sm"
id="audio_input_selectbox"
aria-label=".form-select-sm"
v-html="audio.getInputDevices()"
></select>
</div>
<div class="input-group input-group-sm">
<span class="input-group-text">
<i class="bi bi-volume-up" style="font-size: 1rem"></i>
</span>
<select
class="form-select form-select-sm"
id="audio_output_selectbox"
aria-label=".form-select-sm"
v-html="audio.getOutputDevices()"
></select>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,251 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
</script>
<template>
<nav
class="navbar fixed-bottom navbar-expand-xl bg-body-tertiary border-top p-2"
style="margin-left: 87px"
>
<div class="col">
<div class="btn-toolbar" role="toolbar" style="margin-left: 2px">
<div class="btn-group btn-group-sm me-1" role="group">
<button
class="btn btn-sm btn-secondary me-1"
v-bind:class="{
'bg-danger': state.ptt_state === 'True',
'bg-secondary': state.ptt_state === 'False',
}"
id="ptt_state"
type="button"
title="Rig PTT state"
style="pointer-events: auto"
disabled
>
<i class="bi bi-broadcast-pin" style="font-size: 0.8rem"></i>
</button>
<button
class="btn btn-sm btn-secondary me-1"
id="busy_state"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
v-bind:class="{
'bg-danger': state.busy_state === 'BUSY',
'bg-secondary': state.busy_state === 'IDLE',
}"
title="Modem state"
disabled
style="pointer-events: auto"
>
<i class="bi bi-cpu" style="font-size: 0.8rem"></i>
</button>
<button
class="btn btn-sm btn-secondary me-1"
id="arq_session"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
v-bind:class="{
'bg-secondary': state.arq_session_state === 'disconnected',
'bg-warning': state.arq_session_state === 'connected',
}"
disabled
style="pointer-events: auto"
title="Session state"
>
<i class="bi bi-arrow-left-right" style="font-size: 0.8rem"></i>
</button>
<button
class="btn btn-sm btn-secondary me-1"
id="arq_state"
type="button"
title="Data channel state"
v-bind:class="{
'bg-secondary': state.arq_state === 'False',
'bg-warning': state.arq_state === 'True',
}"
disabled
style="pointer-events: auto"
>
<i class="bi bi-file-earmark-binary" style="font-size: 0.8rem"></i>
</button>
<!--
<button
class="btn btn-sm btn-secondary me-1"
id="rigctld_state"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
title="rigctld state: <strong class='text-success'>CONNECTED</strong> / <strong class='text-secondary'>UNKNOWN</strong>"
>
<i class="bi bi-usb-symbol" style="font-size: 0.8rem"></i>
</button>
-->
<button
class="btn btn-sm disabled me-3"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
v-bind:class="{
'btn-warning': state.channel_busy === 'True',
'btn-secondary': state.channel_busy === 'False',
}"
style="pointer-events: auto"
title="Channel busy"
>
<i class="bi bi-hourglass"></i>
</button>
</div>
<div class="btn-group btn-group-sm me-1" role="group">
<button
class="btn btn-sm btn-secondary me-4 disabled"
type="button"
title="What's the frequency, Kenneth?"
style="pointer-events: auto"
>
{{ parseInt(state.frequency) / 1000 }} KHz
</button>
</div>
<div class="btn-group btn-group-sm me-1" role="group">
<button
class="btn btn-sm btn-secondary me-0"
type="button"
title="Speed level"
>
<i class="bi bi-speedometer2" style="font-size: 1rem"></i>
</button>
<button
class="btn btn-sm btn-secondary me-4 disabled"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
>
<i
class="bi"
style="font-size: 1rem"
v-bind:class="{
'bi-reception-0': state.speed_level == 0,
'bi-reception-1': state.speed_level == 1,
'bi-reception-2': state.speed_level == 2,
'bi-reception-3': state.speed_level == 3,
'bi-reception-4': state.speed_level == 4,
}"
></i>
</button>
</div>
<div class="btn-group btn-group-sm me-1" role="group">
<button
class="btn btn-sm btn-secondary me-0"
type="button"
title="Bytes transfered"
>
<i class="bi bi-file-earmark-binary" style="font-size: 1rem"></i>
</button>
<button
class="btn btn-sm btn-secondary me-4 disabled"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
>
{{ state.arq_total_bytes }}
</button>
</div>
<div class="btn-group btn-group-sm me-1" role="group">
<button
class="btn btn-sm btn-secondary me-0"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
title="Current or last connected with station"
>
<i class="bi bi-file-earmark-binary" style="font-size: 1rem"></i>
</button>
<button
class="btn btn-sm btn-secondary disabled me-1"
type="button"
data-bs-placement="top"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
>
{{ state.dxcallsign }}
</button>
</div>
</div>
</div>
<div class="col-lg-4">
<div style="margin-right: 2px">
<div
class="progress w-100 rounded-0 rounded-top"
style="height: 20px; min-width: 200px"
>
<div
class="progress-bar progress-bar-striped bg-primary force-gpu"
id="transmission_progress"
role="progressbar"
:style="{ width: state.arq_transmission_percent + '%' }"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
></div>
<p
class="justify-content-center m-0 d-flex position-absolute w-100 text-dark"
>
{{ state.arq_seconds_until_finish }}s left
</p>
</div>
<div
class="progress mb-0 rounded-0 rounded-bottom"
style="height: 10px"
>
<div
class="progress-bar progress-bar-striped bg-warning"
id="transmission_timeleft"
role="progressbar"
:style="{ width: state.arq_seconds_until_timeout_percent + '%' }"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
>
<p
class="justify-content-center m-0 d-flex position-absolute w-100 text-dark"
>
timeout in {{ state.arq_seconds_until_timeout }}s
</p>
</div>
</div>
</div>
</div>
</nav>
</template>
ww

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,124 @@
<script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
function saveSettings() {
saveSettingsToFile();
}
</script>
<template>
<div class="card mb-1">
<div class="card-header p-1">
<div class="container">
<div class="row">
<div class="col-1">
<i class="bi bi-house-door" style="font-size: 1.2rem"></i>
</div>
<div class="col-10">
<strong class="fs-5">My station</strong>
</div>
<div class="col-1 text-end">
<button
type="button"
id="openHelpModalStation"
data-bs-toggle="modal"
data-bs-target="#stationHelpModal"
class="btn m-0 p-0 border-0"
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div>
</div>
</div>
</div>
<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-trigger="hover"
data-bs-html="false"
title="Enter your callsign and save it"
>
<span class="input-group-text">
<i class="bi bi-person-bounding-box" style="font-size: 1rem"></i>
</span>
<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"
v-model="settings.mycall"
@input="saveSettings"
/>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="myCallSSID"
v-model="settings.myssid"
@change="saveSettings"
>
<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>
</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-trigger="hover"
data-bs-html="false"
title="Enter your gridsquare and save it"
>
<span class="input-group-text">
<i class="bi bi-house-fill" style="font-size: 1rem"></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"
v-model="settings.mygrid"
@input="saveSettings"
/>
</div>
</div>
</div>
<!-- end of row-->
</div>
</div>
</template>

View file

@ -0,0 +1,276 @@
<script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler";
import { startRigctld, stopRigctld } from "../js/daemon";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
function startStopRigctld() {
switch (state.rigctld_started) {
case "stopped":
settings.hamlib_deviceport = (<HTMLInputElement>document.getElementById("hamlib_deviceport")).value;
startRigctld();
break;
case "running":
stopRigctld();
break;
default:
}
}
function selectRadioControl() {
// @ts-expect-error
switch (event.target.id) {
case "list-rig-control-none-list":
settings.radiocontrol = "disabled";
break;
case "list-rig-control-rigctld-list":
settings.radiocontrol = "rigctld";
break;
case "list-rig-control-tci-list":
settings.radiocontrol = "tci";
break;
default:
console.log("default=!==");
settings.radiocontrol = "disabled";
}
saveSettingsToFile();
}
function testHamlib(){
console.log("not yet implemented")
alert("not yet implemented")
}
</script>
<template>
<div class="card mb-0">
<div class="card-header p-1">
<div class="container">
<div class="row">
<div class="col-1">
<i class="bi bi-projector" style="font-size: 1.2rem"></i>
</div>
<div class="col-4">
<strong class="fs-5">Rig control</strong>
</div>
<div class="col-6">
<div
class="list-group list-group-horizontal"
id="rig-control-list-tab"
role="rig-control-tablist"
>
<a
class="py-1 ps-1 pe-1 list-group-item list-group-item-action"
id="list-rig-control-none-list"
data-bs-toggle="list"
href="#list-rig-control-none"
role="tab"
aria-controls="list-rig-control-none"
v-bind:class="{ active: settings.radiocontrol === 'disabled' }"
@click="selectRadioControl()"
>None/Vox</a
>
<a
class="py-1 ps-1 pe-1 list-group-item list-group-item-action"
id="list-rig-control-rigctld-list"
data-bs-toggle="list"
href="#list-rig-control-rigctld"
role="tab"
aria-controls="list-rig-control-rigctld"
v-bind:class="{ active: settings.radiocontrol === 'rigctld' }"
@click="selectRadioControl()"
>Rigctld</a
>
<a
class="py-1 ps-1 pe-1 list-group-item list-group-item-action"
id="list-rig-control-tci-list"
data-bs-toggle="list"
href="#list-rig-control-tci"
role="tab"
aria-controls="list-rig-control-tci"
v-bind:class="{ active: settings.radiocontrol === 'tci' }"
@click="selectRadioControl()"
>TCI</a
>
</div>
</div>
<div class="col-1 text-end">
<button
type="button"
id="openHelpModalRigControl"
data-bs-toggle="modal"
data-bs-target="#rigcontrolHelpModal"
class="btn m-0 p-0 border-0"
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body p-2" style="height: 100px">
<div class="tab-content" id="rig-control-nav-tabContent">
<div
class="tab-pane fade"
v-bind:class="{ 'show active': settings.radiocontrol === 'disabled' }"
id="list-rig-control-none"
role="tabpanel"
aria-labelledby="list-rig-control-none-list"
>
<p class="small">
Modem will not utilize rig control and features will be limited. While
functional; it is recommended to configure hamlib.
</p>
</div>
<div
class="tab-pane fade"
id="list-rig-control-rigctld"
v-bind:class="{ 'show active': settings.radiocontrol === 'rigctld' }"
role="tabpanel"
aria-labelledby="list-rig-control-rigctld-list"
>
<div class="input-group input-group-sm mb-1">
<div class="input-group input-group-sm mb-1">
<span class="input-group-text"> Radio port </span>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="hamlib_deviceport"
style="width: 7rem"
v-html="settings.getSerialDevices()"
>
</select>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text">Rigctld service</span>
<button
class="btn btn-outline-success"
type="button"
id="hamlib_rigctld_start"
@click="startStopRigctld"
>
Start
</button>
<button
class="btn btn-outline-danger"
type="button"
id="hamlib_rigctld_stop"
@click="startStopRigctld"
>
Stop
</button>
<input
type="text"
class="form-control"
placeholder="Status"
id="hamlib_rigctld_status"
aria-label="State"
aria-describedby="basic-addon1"
v-model="state.rigctld_started"
/>
<button
type="button"
id="testHamlib"
class="btn btn-sm btn-outline-secondary ms-1"
data-bs-placement="bottom"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="true"
@click="testHamlib"
title="Test your hamlib settings and toggle PTT once. Button will become <strong class='text-success'>green</strong> on success and <strong class='text-danger'>red</strong> if fails."
>
PTT Test
</button>
</div>
</div>
</div>
<div
class="tab-pane fade"
id="list-rig-control-tci"
v-bind:class="{ 'show active': settings.radiocontrol === 'tci' }"
role="tabpanel"
aria-labelledby="list-rig-control-tci-list"
>
<div class="input-group input-group-sm mb-1">
<div class="input-group input-group-sm mb-1">
<span class="input-group-text">TCI</span>
<span class="input-group-text">Address</span>
<input
type="text"
class="form-control"
placeholder="tci IP"
id="tci_ip"
aria-label="Device IP"
v-model="settings.tci_ip"
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text">Port</span>
<input
type="text"
class="form-control"
placeholder="tci port"
id="tci_port"
aria-label="Device Port"
v-model="settings.tci_port"
/>
</div>
</div>
</div>
</div>
<!-- RADIO CONTROL DISABLED -->
<div id="radio-control-disabled"></div>
<!-- RADIO CONTROL RIGCTLD -->
<div id="radio-control-rigctld"></div>
<!-- RADIO CONTROL TCI-->
<div id="radio-control-tci"></div>
<!-- RADIO CONTROL HELP -->
<div id="radio-control-help">
<!--
<strong>VOX:</strong> Use rig control mode 'none'
<br />
<strong>HAMLIB locally:</strong> configure in settings, then
start/stop service.
<br />
<strong>HAMLIB remotely:</strong> Enter IP/Port, connection
happens automatically.
-->
</div>
</div>
<!--<div class="card-footer text-muted small" id="hamlib_info_field">
Define Modem rig control mode (none/hamlib)
</div>
-->
</div>
</template>

View file

@ -0,0 +1,128 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { useAudioStore } from "../store/audioStore.js";
const audioStore = useAudioStore(pinia);
import { startModem, stopModem } from "../js/daemon";
import { saveSettingsToFile } from "../js/settingsHandler";
function startStopModem() {
switch (state.modem_running_state) {
case "stopped":
let startupInputDeviceValue = parseInt((<HTMLSelectElement>document.getElementById("audio_input_selectbox")).value);
let startupOutputDeviceValue = parseInt((<HTMLSelectElement>document.getElementById("audio_output_selectbox")).value);
let startupInputDeviceIndex = (<HTMLSelectElement>document.getElementById("audio_input_selectbox")).selectedIndex;
let startupOutputDeviceIndex = (<HTMLSelectElement>document.getElementById("audio_output_selectbox")).selectedIndex;
audioStore.startupInputDevice = startupInputDeviceValue
audioStore.startupOutputDevice = startupOutputDeviceValue
// get full name of audio device
settings.rx_audio = (<HTMLSelectElement>document.getElementById("audio_input_selectbox")).options[startupInputDeviceIndex].text;
settings.tx_audio = (<HTMLSelectElement>document.getElementById("audio_output_selectbox")).options[startupOutputDeviceIndex].text;
saveSettingsToFile();
startModem();
break;
case "running":
stopModem();
break;
default:
}
}
</script>
<template>
<nav class="navbar bg-body-tertiary border-bottom">
<div class="mx-auto">
<span class="badge bg-secondary me-4"
>Modem location | {{ settings.modem_host }}</span
>
<span class="badge bg-secondary me-4"
>Service | {{ state.modem_running_state }}</span
>
<div class="btn-group" role="group"></div>
<div class="btn-group me-4" role="group">
<button
type="button"
id="startModem"
class="btn btn-sm btn-outline-success"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="false"
title="Start the Modem. Please set your audio and radio settings first!"
@click="startStopModem()"
v-bind:class="{ disabled: state.modem_running_state === 'running' }"
>
<i class="bi bi-play-fill"></i>
<span class="ms-2">start modem</span>
</button>
<button
type="button"
id="stopModem"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="false"
title="Stop the Modem."
@click="startStopModem()"
v-bind:class="{ disabled: state.modem_running_state === 'stopped' }"
>
<i class="bi bi-stop-fill"></i>
<span class="ms-2">stop modem</span>
</button>
</div>
<button
type="button"
id="openHelpModalStartStopModem"
data-bs-toggle="modal"
data-bs-target="#startStopModemHelpModal"
class="btn me-4 p-0 border-0"
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div>
<!--
<div class="btn-toolbar" role="toolbar">
<span data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-trigger="hover" data-bs-html="false"
title="View the received files. This is currently under development!">
<button class="btn btn-sm btn-primary me-2" data-bs-toggle="offcanvas" data-bs-target="#receivedFilesSidebar" id="openReceivedFiles" type="button" > <strong>Files </strong>
<i class="bi bi-file-earmark-arrow-up-fill" style="font-size: 1rem; color: white;"></i>
<i class="bi bi-file-earmark-arrow-down-fill" style="font-size: 1rem; color: white;"></i>
</button>
</span> <span data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-trigger="hover" data-bs-html="false" title="Send files through HF. This is currently under development!">
<button class="btn btn-sm btn-primary me-2" id="openDataModule" data-bs-toggle="offcanvas" data-bs-target="#transmitFileSidebar" type="button" style="display: None;"> <strong>TX File </strong>
<i class="bi bi-file-earmark-arrow-up-fill" style="font-size: 1rem; color: white;"></i>
</button>
</span> <span data-bs-placement="bottom" data-bs-toggle="tooltip" data-bs-trigger="hover" data-bs-html="true"
title="Settings and Info">
</span>
</div>
--></nav>
</template>

View file

@ -0,0 +1,87 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
</script>
<template>
<div class="card mb-0">
<div class="card-header p-1 d-flex">
<div class="container">
<div class="row">
<div class="col-1">
<i class="bi bi-cloud-download" style="font-size: 1.2rem"></i>
</div>
<div class="col-3">
<strong class="fs-5">Updater</strong>
</div>
<div class="col-7">
<div class="progress w-100 ms-1 m-1">
<div
class="progress-bar"
style="width: 0%"
role="progressbar"
id="UpdateProgressBar"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
>
<span id="UpdateProgressInfo"></span>
</div>
</div>
</div>
<div class="col-1 text-end">
<button
type="button"
id="openHelpModalUpdater"
data-bs-toggle="modal"
data-bs-target="#updaterHelpModal"
class="btn m-0 p-0 border-0"
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body p-2 mb-1">
<button
class="btn btn-secondary btn-sm ms-1 me-1"
id="updater_channel"
type="button"
disabled
>
{{ settings.update_channel }}
</button>
<button
class="btn btn-secondary btn-sm ms-1"
id="updater_status"
type="button"
disabled
>
...
</button>
<button
class="btn btn-secondary btn-sm ms-1"
id="updater_changelog"
type="button"
data-bs-toggle="modal"
data-bs-target="#updaterReleaseNotes"
>
Changelog
</button>
<button
class="btn btn-primary btn-sm ms-1"
id="update_and_install"
type="button"
style="display: none"
>
Install & Restart
</button>
</div>
</div>
</template>

View file

@ -0,0 +1,171 @@
<script setup lang="ts">
import settings_gui from "./settings_gui.vue";
import settings_chat from "./settings_chat.vue";
import settings_hamlib from "./settings_hamlib.vue";
import settings_modem from "./settings_modem.vue";
import settings_web from "./settings_web.vue";
import settings_exp from "./settings_exp.vue";
</script>
<template>
<div
class="tab-pane fade"
id="list-settings"
role="tabpanel"
aria-labelledby="list-settings-list"
>
<div class="container">
<div class="badge text-bg-warning ms-3">
<i class="bi bi-exclamation-triangle"></i> Please restart the modem
after changing settings!
</div>
<!-- SETTINGS Nav tabs -->
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<button
class="nav-link active"
id="gui-tab"
data-bs-toggle="tab"
data-bs-target="#gui"
type="button"
role="tab"
aria-controls="home"
aria-selected="true"
>
GUI
</button>
</li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
id="chat-tab"
data-bs-toggle="tab"
data-bs-target="#chat"
type="button"
role="tab"
aria-controls="home"
aria-selected="true"
>
Chat
</button>
</li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
id="hamlib-tab"
data-bs-toggle="tab"
data-bs-target="#hamlib"
type="button"
role="tab"
aria-controls="profile"
aria-selected="false"
>
Hamlib
</button>
</li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
id="modem-tab"
data-bs-toggle="tab"
data-bs-target="#modem"
type="button"
role="tab"
aria-controls="profile"
aria-selected="false"
>
Modem
</button>
</li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
id="web-tab"
data-bs-toggle="tab"
data-bs-target="#web"
type="button"
role="tab"
aria-controls="messages"
aria-selected="false"
>
Web
</button>
</li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
id="experiments-tab"
data-bs-toggle="tab"
data-bs-target="#experiments"
type="button"
role="tab"
aria-controls="settings"
aria-selected="false"
>
Exp
</button>
</li>
</ul>
<!-- SETTINGS Nav Tab panes -->
<div class="tab-content mt-1">
<!-- GUI tab contents-->
<div
class="tab-pane active"
id="gui"
role="tabpanel"
aria-labelledby="gui-tab"
tabindex="0"
>
<settings_gui />
</div>
<div
class="tab-pane"
id="chat"
role="tabpanel"
aria-labelledby="chat-tab"
tabindex="0"
>
<settings_chat />
</div>
<div
class="tab-pane"
id="hamlib"
role="tabpanel"
aria-labelledby="hamlib-tab"
tabindex="0"
>
<settings_hamlib />
</div>
<div
class="tab-pane"
id="modem"
role="tabpanel"
aria-labelledby="modem-tab"
tabindex="0"
>
<settings_modem />
</div>
<div
class="tab-pane"
id="web"
role="tabpanel"
aria-labelledby="web-tab"
tabindex="0"
>
<settings_web />
</div>
<div
class="tab-pane"
id="experiments"
role="tabpanel"
aria-labelledby="experiments-tab"
tabindex="0"
>
<settings_exp />
</div>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,128 @@
<script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
function saveSettings() {
saveSettingsToFile();
}
</script>
<template>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Enable "is typing"</label>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline">
<input
class="form-check-input"
type="checkbox"
id="enable_is_writing"
@change="saveSettings"
v-model="settings.enable_is_writing"
true-value="True"
false-value="False"
disabled
/>
<label class="form-check-label" for="GraphicsSwitch"
>Additional broadcast burst</label
>
</div>
</label>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Allow requesting "user profile"</label>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline">
<input
class="form-check-input"
type="checkbox"
id="enable_request_profile"
@change="saveSettings"
v-model="settings.enable_request_profile"
true-value="True"
false-value="False"
disabled
/>
</div>
</label>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50"
>Allow requesting "shared folder"</label
>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline">
<input
class="form-check-input"
type="checkbox"
id="enable_request_shared_folder"
@change="saveSettings"
v-model="settings.enable_request_shared_folder"
true-value="True"
false-value="False"
disabled
/>
</div>
</label>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Shared folder path</label>
<input
type="text"
class="form-control w-50"
id="shared_folder_path"
@change="saveSettings"
v-model="settings.shared_folder_path"
disabled
/>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50"
>Enable auto retry on Beacon or Ping
</label>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline">
<input
class="form-check-input"
type="checkbox"
id="enable_auto_retry"
@change="saveSettings"
v-model="settings.enable_auto_retry"
true-value="True"
false-value="False"
/>
</div>
</label>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text w-50">message retry attempts</span>
<select
class="form-select form-select-sm w-50"
id="max_retry_attempts"
@change="saveSettings"
v-model="settings.max_retry_attempts"
disabled
>
<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>
</select>
</div>
</template>

View file

@ -0,0 +1,99 @@
<script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
function saveSettings() {
saveSettingsToFile();
}
</script>
<template>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Enable autotune</label>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline ms-2">
<input
class="form-check-input"
type="checkbox"
id="autoTuneSwitch"
@change="saveSettings"
v-model="settings.auto_tune"
true-value="True"
false-value="False"
/>
<label class="form-check-label" for="autoTuneSwitch"
>adjust ALC on TX</label
>
</div>
</label>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Enable FSK mode</label>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline ms-2">
<input
class="form-check-input"
type="checkbox"
id="fskModeSwitch"
@change="saveSettings"
v-model="settings.enable_fsk"
true-value="True"
false-value="False"
disabled
/>
<label class="form-check-label" for="fskModeSwitch"
>not available, yet</label
>
</div>
</label>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Enable MESH protocol</label>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline ms-2">
<input
class="form-check-input"
type="checkbox"
id="enableMeshSwitch"
@change="saveSettings"
v-model="settings.enable_mesh_features"
true-value="True"
false-value="False"
/>
<label class="form-check-label" for="enableMeshSwitch"
>experimental! REALLY!</label
>
</div>
</label>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Database maintenance</label>
<label class="input-group-text w-50">
<button
class="btn btn-outline-secondary btn-sm w-50"
id="btnCleanDB"
type="button"
disabled
>
Clean</button
>&nbsp;
<div
class="spinner-border text-warning invisible"
role="status"
id="divCleanDBSpinner"
></div>
</label>
</div>
<div class="center">
<div class="badge text-bg-danger">
<i class="bi bi-shield-exclamation"></i> These options may not work and
are for experienced users only!
</div>
</div>
</template>

View file

@ -0,0 +1,158 @@
<script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
function saveSettings() {
saveSettingsToFile();
}
</script>
<template>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text w-50">GUI theme</span>
<select
class="form-select form-select-sm w-50"
id="theme_selector"
@change="saveSettings"
v-model="settings.theme"
disabled
>
<option value="default_light">Default (light)</option>
<option value="default_dark">Default (dark)</option>
<option value="default_auto">Default (auto)</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 w-50">Waterfall theme</span>
<select
class="form-select form-select-sm w-50"
id="wftheme_selector"
@change="saveSettings"
v-model="settings.wftheme"
disabled
>
<option value="2">Default</option>
<option value="0">Turbo</option>
<option value="1">Fosphor</option>
<option value="3">Inferno</option>
<option value="4">Magma</option>
<option value="5">Jet</option>
<option value="6">Binary</option>
</select>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Enable fancy GUI</label>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline">
<input
class="form-check-input"
type="checkbox"
id="GraphicsSwitch"
@change="saveSettings"
v-model="settings.high_graphics"
true-value="True"
false-value="False"
disabled
/>
<label class="form-check-label" for="GraphicsSwitch"
>Higher CPU Usage</label
>
</div>
</label>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50" for="inputGroupFile02"
>Received files folder</label
>
<input
type="text"
class="form-control w-50"
id="received_files_folder"
disabled
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text w-50">Update channel</span>
<select
class="form-select form-select-sm w-50"
id="update_channel_selector"
@change="saveSettings"
v-model="settings.update_channel"
disabled
>
<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 w-50">Enable notifications</label>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline">
<input
class="form-check-input"
type="checkbox"
id="NotificationSwitch"
@change="saveSettings"
v-model="settings.enable_sys_notification"
true-value="True"
false-value="False"
disabled
/>
<label class="form-check-label" for="NotificationSwitch"
>Show system pop-ups</label
>
</div>
</label>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Auto-start Modem/rigctld</label>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline">
<input
class="form-check-input"
type="checkbox"
id="AutoStartSwitch"
@change="saveSettings"
v-model="settings.auto_start"
true-value="True"
false-value="False"
disabled
/>
<label class="form-check-label" for="AutoStartSwitch"
>Start on app launch</label
>
</div>
</label>
</div>
</template>

View file

@ -0,0 +1,522 @@
<script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
function saveSettings() {
saveSettingsToFile();
}
</script>
<template>
<hr class="m-2" />
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">Rigctld path</span>
<input
type="text"
class="form-control"
placeholder="rigctld Path"
id="hamlib_rigctld_path"
aria-label="Device IP"
aria-describedby="basic-addon1"
@change="saveSettings"
v-model="settings.hamlib_rigctld_path"
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px"
>Rigctld server port</span
>
<input
type="text"
class="form-control"
placeholder="rigctld port"
id="hamlib_rigctld_server_port"
aria-label="Device Port"
aria-describedby="basic-addon1"
@change="saveSettings"
v-model="settings.hamlib_rigctld_server_port"
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">Rigctld remote ip</span>
<input
type="text"
class="form-control"
placeholder="rigctld IP"
id="hamlib_rigctld_ip"
aria-label="Device IP"
v-model="settings.hamlib_rigctld_ip"
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px"
>Rigctld remote port</span
>
<input
type="text"
class="form-control"
placeholder="rigctld port"
id="hamlib_rigctld_port"
aria-label="Device Port"
v-model="settings.hamlib_rigctld_port"
/>
</div>
<hr class="m-2" />
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px"> Radio model </span>
<input
class="form-control"
list="datalistOptions"
id="hamlib_deviceid"
placeholder="Search radio..."
style="width: 7rem"
@change="saveSettings"
v-model="settings.hamlib_deviceid"
/>
<datalist id="datalistOptions">
<option selected value="-- ignore --">-- ignore --</option>
<option value="2028">Kenwood TS480</option>
<option value="1">Hamlib Dummy</option>
<option value="2">Hamlib NET rigctl</option>
<option value="4">FLRig FLRig</option>
<option value="5">TRXManager TRXManager 5.7.630+</option>
<option value="6">Hamlib Dummy No VFO</option>
<option value="1001">Yaesu FT-847</option>
<option value="1003">Yaesu FT-1000D</option>
<option value="1004">Yaesu MARK-V FT-1000MP</option>
<option value="1005">Yaesu FT-747GX</option>
<option value="1006">Yaesu FT-757GX</option>
<option value="1007">Yaesu FT-757GXII</option>
<option value="1009">Yaesu FT-767GX</option>
<option value="1010">Yaesu FT-736R</option>
<option value="1011">Yaesu FT-840</option>
<option value="1013">Yaesu FT-900</option>
<option value="1014">Yaesu FT-920</option>
<option value="1015">Yaesu FT-890</option>
<option value="1016">Yaesu FT-990</option>
<option value="1017">Yaesu FRG-100</option>
<option value="1018">Yaesu FRG-9600</option>
<option value="1019">Yaesu FRG-8800</option>
<option value="1020">Yaesu FT-817</option>
<option value="1021">Yaesu FT-100</option>
<option value="1022">Yaesu FT-857</option>
<option value="1023">Yaesu FT-897</option>
<option value="1024">Yaesu FT-1000MP</option>
<option value="1025">Yaesu MARK-V Field FT-1000MP</option>
<option value="1026">Yaesu VR-5000</option>
<option value="1027">Yaesu FT-450</option>
<option value="1028">Yaesu FT-950</option>
<option value="1029">Yaesu FT-2000</option>
<option value="1030">Yaesu FTDX-9000</option>
<option value="1031">Yaesu FT-980</option>
<option value="1032">Yaesu FTDX-5000</option>
<option value="1033">Vertex Standard VX-1700</option>
<option value="1034">Yaesu FTDX-1200</option>
<option value="1035">Yaesu FT-991</option>
<option value="1036">Yaesu FT-891</option>
<option value="1037">Yaesu FTDX-3000</option>
<option value="1038">Yaesu FT-847UNI</option>
<option value="1039">Yaesu FT-600</option>
<option value="1040">Yaesu FTDX-101D</option>
<option value="1041">Yaesu FT-818</option>
<option value="1042">Yaesu FTDX-10</option>
<option value="1043">Yaesu FT-897D</option>
<option value="1044">Yaesu FTDX-101MP</option>
<option value="2001">Kenwood TS-50S</option>
<option value="2002">Kenwood TS-440S</option>
<option value="2003">Kenwood TS-450S</option>
<option value="2004">Kenwood TS-570D</option>
<option value="2005">Kenwood TS-690S</option>
<option value="2006">Kenwood TS-711</option>
<option value="2007">Kenwood TS-790</option>
<option value="2008">Kenwood TS-811</option>
<option value="2009">Kenwood TS-850</option>
<option value="2010">Kenwood TS-870S</option>
<option value="2011">Kenwood TS-940S</option>
<option value="2012">Kenwood TS-950S</option>
<option value="2013">Kenwood TS-950SDX</option>
<option value="2014">Kenwood TS-2000</option>
<option value="2015">Kenwood R-5000</option>
<option value="2016">Kenwood TS-570S</option>
<option value="2017">Kenwood TH-D7A</option>
<option value="2019">Kenwood TH-F6A</option>
<option value="2020">Kenwood TH-F7E</option>
<option value="2021">Elecraft K2</option>
<option value="2022">Kenwood TS-930</option>
<option value="2023">Kenwood TH-G71</option>
<option value="2024">Kenwood TS-680S</option>
<option value="2025">Kenwood TS-140S</option>
<option value="2026">Kenwood TM-D700</option>
<option value="2027">Kenwood TM-V7</option>
<option value="2028">Kenwood TS-480</option>
<option value="2029">Elecraft K3</option>
<option value="2030">Kenwood TRC-80</option>
<option value="2031">Kenwood TS-590S</option>
<option value="2032">SigFox Transfox</option>
<option value="2033">Kenwood TH-D72A</option>
<option value="2034">Kenwood TM-D710(G)</option>
<option value="2036">FlexRadio 6xxx</option>
<option value="2037">Kenwood TS-590SG</option>
<option value="2038">Elecraft XG3</option>
<option value="2039">Kenwood TS-990s</option>
<option value="2040">OpenHPSDR PiHPSDR</option>
<option value="2041">Kenwood TS-890S</option>
<option value="2042">Kenwood TH-D74</option>
<option value="2043">Elecraft K3S</option>
<option value="2044">Elecraft KX2</option>
<option value="2045">Elecraft KX3</option>
<option value="2046">Hilberling PT-8000A</option>
<option value="2047">Elecraft K4</option>
<option value="2048">FlexRadio/ANAN PowerSDR/Thetis</option>
<option value="2049">Malachite DSP</option>
<option value="3002">Icom IC-1275</option>
<option value="3003">Icom IC-271</option>
<option value="3004">Icom IC-275</option>
<option value="3006">Icom IC-471</option>
<option value="3007">Icom IC-475</option>
<option value="3009">Icom IC-706</option>
<option value="3010">Icom IC-706MkII</option>
<option value="3011">Icom IC-706MkIIG</option>
<option value="3012">Icom IC-707</option>
<option value="3013">Icom IC-718</option>
<option value="3014">Icom IC-725</option>
<option value="3015">Icom IC-726</option>
<option value="3016">Icom IC-728</option>
<option value="3017">Icom IC-729</option>
<option value="3019">Icom IC-735</option>
<option value="3020">Icom IC-736</option>
<option value="3021">Icom IC-737</option>
<option value="3022">Icom IC-738</option>
<option value="3023">Icom IC-746</option>
<option value="3024">Icom IC-751</option>
<option value="3026">Icom IC-756</option>
<option value="3027">Icom IC-756PRO</option>
<option value="3028">Icom IC-761</option>
<option value="3029">Icom IC-765</option>
<option value="3030">Icom IC-775</option>
<option value="3031">Icom IC-781</option>
<option value="3032">Icom IC-820H</option>
<option value="3034">Icom IC-821H</option>
<option value="3035">Icom IC-970</option>
<option value="3036">Icom IC-R10</option>
<option value="3037">Icom IC-R71</option>
<option value="3038">Icom IC-R72</option>
<option value="3039">Icom IC-R75</option>
<option value="3040">Icom IC-R7000</option>
<option value="3041">Icom IC-R7100</option>
<option value="3042">Icom ICR-8500</option>
<option value="3043">Icom IC-R9000</option>
<option value="3044">Icom IC-910</option>
<option value="3045">Icom IC-78</option>
<option value="3046">Icom IC-746PRO</option>
<option value="3047">Icom IC-756PROII</option>
<option value="3051">Ten-Tec Omni VI Plus</option>
<option value="3052">Optoelectronics OptoScan535</option>
<option value="3053">Optoelectronics OptoScan456</option>
<option value="3054">Icom IC ID-1</option>
<option value="3055">Icom IC-703</option>
<option value="3056">Icom IC-7800</option>
<option value="3057">Icom IC-756PROIII</option>
<option value="3058">Icom IC-R20</option>
<option value="3060">Icom IC-7000</option>
<option value="3061">Icom IC-7200</option>
<option value="3062">Icom IC-7700</option>
<option value="3063">Icom IC-7600</option>
<option value="3064">Ten-Tec Delta II</option>
<option value="3065">Icom IC-92D</option>
<option value="3066">Icom IC-R9500</option>
<option value="3067">Icom IC-7410</option>
<option value="3068">Icom IC-9100</option>
<option value="3069">Icom IC-RX7</option>
<option value="3070">Icom IC-7100</option>
<option value="3071">Icom ID-5100</option>
<option value="3072">Icom IC-2730</option>
<option value="3073">Icom IC-7300</option>
<option value="3074">Microtelecom Perseus</option>
<option value="3075">Icom IC-785x</option>
<option value="3076">Xeigu X108G</option>
<option value="3077">Icom IC-R6</option>
<option value="3078">Icom IC-7610</option>
<option value="3079">Icom IC-R8600</option>
<option value="3080">Icom IC-R30</option>
<option value="3081">Icom IC-9700</option>
<option value="3082">Icom ID-4100</option>
<option value="3083">Icom ID-31</option>
<option value="3084">Icom ID-51</option>
<option value="3085">Icom IC-705</option>
<option value="4001">Icom IC-PCR1000</option>
<option value="4002">Icom IC-PCR100</option>
<option value="4003">Icom IC-PCR1500</option>
<option value="4004">Icom IC-PCR2500</option>
<option value="5001">AOR AR8200</option>
<option value="5002">AOR AR8000</option>
<option value="5003">AOR AR7030</option>
<option value="5004">AOR AR5000</option>
<option value="5005">AOR AR3030</option>
<option value="5006">AOR AR3000A</option>
<option value="5008">AOR AR2700</option>
<option value="5013">AOR AR8600</option>
<option value="5014">AOR AR5000A</option>
<option value="5015">AOR AR7030 Plus</option>
<option value="5016">AOR SR2200</option>
<option value="6005">JRC NRD-525</option>
<option value="6006">JRC NRD-535D</option>
<option value="6007">JRC NRD-545 DSP</option>
<option value="8001">Uniden BC780xlt</option>
<option value="8002">Uniden BC245xlt</option>
<option value="8003">Uniden BC895xlt</option>
<option value="8004">Radio Shack PRO-2052</option>
<option value="8006">Uniden BC250D</option>
<option value="8010">Uniden BCD-396T</option>
<option value="8011">Uniden BCD-996T</option>
<option value="8012">Uniden BC898T</option>
<option value="9002">Drake R-8A</option>
<option value="9003">Drake R-8B</option>
<option value="10004">Lowe HF-235</option>
<option value="11003">Racal RA6790/GM</option>
<option value="11005">Racal RA3702</option>
<option value="12004">Watkins-Johnson WJ-8888</option>
<option value="14002">Skanti TRP8000</option>
<option value="14004">Skanti TRP 8255 S R</option>
<option value="15001">Winradio WR-1000</option>
<option value="15002">Winradio WR-1500</option>
<option value="15003">Winradio WR-1550</option>
<option value="15004">Winradio WR-3100</option>
<option value="15005">Winradio WR-3150</option>
<option value="15006">Winradio WR-3500</option>
<option value="15007">Winradio WR-3700</option>
<option value="15009">Winradio WR-G313</option>
<option value="16001">Ten-Tec TT-550</option>
<option value="16002">Ten-Tec TT-538 Jupiter</option>
<option value="16003">Ten-Tec RX-320</option>
<option value="16004">Ten-Tec RX-340</option>
<option value="16005">Ten-Tec RX-350</option>
<option value="16007">Ten-Tec TT-516 Argonaut V</option>
<option value="16008">Ten-Tec TT-565 Orion</option>
<option value="16009">Ten-Tec TT-585 Paragon</option>
<option value="16011">Ten-Tec TT-588 Omni VII</option>
<option value="16012">Ten-Tec RX-331</option>
<option value="16013">Ten-Tec TT-599 Eagle</option>
<option value="17001">Alinco DX-77</option>
<option value="17002">Alinco DX-SR8</option>
<option value="18001">Kachina 505DSP</option>
<option value="22001">TAPR DSP-10</option>
<option value="23001">Flex-radio SDR-1000</option>
<option value="23003">DTTS Microwave Society DttSP IPC</option>
<option value="23004">DTTS Microwave Society DttSP UDP</option>
<option value="24001">RFT EKD-500</option>
<option value="25001">Elektor Elektor 3/04</option>
<option value="25002">SAT-Schneider DRT1</option>
<option value="25003">Coding Technologies Digital World Traveller</option>
<option value="25006">AmQRP DDS-60</option>
<option value="25007">Elektor Elektor SDR-USB</option>
<option value="25008">mRS miniVNA</option>
<option value="25009">SoftRock Si570 AVR-USB</option>
<option value="25011">KTH-SDR kit Si570 PIC-USB</option>
<option value="25012">FiFi FiFi-SDR</option>
<option value="25013">AMSAT-UK FUNcube Dongle</option>
<option value="25014">N2ADR HiQSDR</option>
<option value="25015">Funkamateur FA-SDR</option>
<option value="25016">AE9RB Si570 Peaberry V1</option>
<option value="25017">AE9RB Si570 Peaberry V2</option>
<option value="25018">AMSAT-UK FUNcube Dongle Pro+</option>
<option value="25019">HobbyPCB RS-HFIQ</option>
<option value="26001">Video4Linux SW/FM radio</option>
<option value="26002">Video4Linux2 SW/FM radio</option>
<option value="27001">Rohde&Schwarz ESMC</option>
<option value="27002">Rohde&Schwarz EB200</option>
<option value="27003">Rohde&Schwarz XK2100</option>
<option value="28001">Philips/Simoco PRM8060</option>
<option value="29001">ADAT www.adat.ch ADT-200A</option>
<option value="30001">Icom IC-M700PRO</option>
<option value="30002">Icom IC-M802</option>
<option value="30003">Icom IC-M710</option>
<option value="30004">Icom IC-M803</option>
<option value="31001">Dorji DRA818V</option>
<option value="31002">Dorji DRA818U</option>
<option value="32001">Barrett 2050</option>
<option value="32002">Barrett 950</option>
<option value="33001">ELAD FDM-DUO</option>
</datalist>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">Serial speed</span>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="hamlib_serialspeed"
@change="saveSettings"
v-model="settings.hamlib_serialspeed"
>
<option selected value="ignore">-- ignore --</option>
<option value="1200">1200</option>
<option value="2400">2400</option>
<option value="4800">4800</option>
<option value="9600">9600</option>
<option value="14400">14400</option>
<option value="19200">19200</option>
<option value="28800">28800</option>
<option value="38400">38400</option>
<option value="57600">57600</option>
<option value="115200">115200</option>
</select>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">Data bits</span>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="hamlib_data_bits"
@change="saveSettings"
v-model="settings.hamlib_data_bits"
>
<option selected value="ignore">-- ignore --</option>
<option value="7">7</option>
<option value="8">8</option>
</select>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">Stop bits</span>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="hamlib_stop_bits"
@change="saveSettings"
v-model="settings.hamlib_stop_bits"
>
<option selected value="ignore">-- ignore --</option>
<option value="1">1</option>
<option value="2">2</option>
</select>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">Serial handshake</span>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="hamlib_handshake"
@change="saveSettings"
v-model="settings.hamlib_handshake"
>
<option selected value="ignore">-- ignore --</option>
<option value="None">None (Default)</option>
</select>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">PTT device port</span>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="hamlib_ptt_port"
@change="saveSettings"
v-model="settings.hamlib_ptt_port"
>
<option selected value="ignore">-- ignore --</option>
</select>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">PTT type</span>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="hamlib_pttprotocol"
style="width: 0.5rem"
@change="saveSettings"
v-model="settings.hamlib_pttprotocol"
>
<option selected value="ignore">-- ignore --</option>
<option value="NONE">NONE</option>
<option value="RIG">RIG</option>
<option value="USB">USB</option>
<option value="RTS">Serial RTS</option>
<option value="PARALLEL">Rig PARALLEL</option>
<option value="MICDATA">Rig MICDATA</option>
<option value="CM108">Rig CM108</option>
</select>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">DCD</span>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="hamlib_dcd"
style="width: 0.5rem"
@change="saveSettings"
v-model="settings.hamlib_dcd"
>
<option selected value="ignore">-- ignore --</option>
<option value="NONE">NONE</option>
<option value="RIG">RIG/CAT</option>
<option value="DSR">DSR</option>
<option value="CTS">CTS</option>
<option value="CD">CD</option>
<option value="PARALLEL">PARALLEL</option>
</select>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">DTR</span>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="hamlib_dtrstate"
style="width: 0.5rem"
@change="saveSettings"
v-model="settings.hamlib_dtrstate"
>
<option selected value="ignore">-- ignore --</option>
<option value="OFF">OFF</option>
<option value="ON">ON</option>
</select>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">Rigctld command</span>
<input
type="text"
class="form-control"
id="hamlib_rigctld_command"
aria-label="Device Port"
aria-describedby="basic-addon1"
disabled
placeholder="auto populated from above settings"
/>
<button
class="btn btn-outline-secondary"
type="button"
id="btnHamlibCopyCommand"
>
<i id="btnHamlibCopyCommandBi" class="bi bi-clipboard"></i>
</button>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px"
>Rigctld custom arguments</span
>
<input
type="text"
class="form-control"
placeholder="not typically needed"
id="hamlib_rigctld_custom_args"
aria-label="Custom arguments"
aria-describedby="basic-addon1"
@change="saveSettings"
v-model="settings.hamlib_rigctld_custom_args"
/>
</div>
</template>

View file

@ -0,0 +1,216 @@
<script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
function saveSettings() {
saveSettingsToFile();
}
</script>
<template>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">modem port</span>
<input
type="text"
class="form-control"
placeholder="modem port"
id="modem_port"
maxlength="5"
max="65534"
min="1025"
@change="saveSettings"
v-model="settings.modem_port"
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">modem host</span>
<input
type="text"
class="form-control"
placeholder="modem host"
id="modem_port"
@change="saveSettings"
v-model="settings.modem_host"
/>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">TX delay in ms</label>
<select
class="form-select form-select-sm"
id="tx_delay"
@change="saveSettings"
v-model="settings.tx_delay"
>
<option value="0">0</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="150">150</option>
<option value="200">200</option>
<option value="250">250</option>
<option value="300">300</option>
<option value="350">350</option>
<option value="400">400</option>
<option value="450">450</option>
<option value="500">500</option>
<option value="550">550</option>
<option value="600">600</option>
<option value="650">650</option>
<option value="700">700</option>
<option value="750">750</option>
<option value="800">800</option>
<option value="850">850</option>
<option value="900">900</option>
<option value="950">950</option>
<option value="1000">1000</option>
</select>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-25">Tuning range</label>
<label class="input-group-text">fmin</label>
<select
class="form-select form-select-sm"
id="tuning_range_fmin"
@change="saveSettings"
v-model="settings.tuning_range_fmin"
>
<option value="-50.0">-50.0</option>
<option value="-100.0">-100.0</option>
<option value="-150.0">-150.0</option>
<option value="-200.0">-200.0</option>
<option value="-250.0">-250.0</option>
</select>
<label class="input-group-text">fmax</label>
<select
class="form-select form-select-sm"
id="tuning_range_fmax"
@change="saveSettings"
v-model="settings.tuning_range_fmax"
>
<option value="50.0">50.0</option>
<option value="100.0">100.0</option>
<option value="150.0">150.0</option>
<option value="200.0">200.0</option>
<option value="250.0">250.0</option>
</select>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text w-50">Beacon interval</span>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="beaconInterval"
style="width: 6rem"
@change="saveSettings"
v-model="settings.beacon_interval"
>
<option value="60">60 secs</option>
<option value="90">90 secs</option>
<option value="120">2 mins</option>
<option selected value="300">5 mins</option>
<option value="600">10 mins</option>
<option value="900">15 mins</option>
<option value="1800">30 mins</option>
<option value="3600">60 mins</option>
</select>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Enable waterfall data</label>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline">
<input
class="form-check-input"
type="checkbox"
id="fftSwitch"
@change="saveSettings"
v-model="settings.enable_fft"
true-value="True"
false-value="False"
/>
<label class="form-check-label" for="fftSwitch">Waterfall</label>
</div>
</label>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Enable scatter diagram data</label>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline">
<input
class="form-check-input"
type="checkbox"
id="scatterSwitch"
@change="saveSettings"
v-model="settings.enable_scatter"
true-value="True"
false-value="False"
/>
<label class="form-check-label" for="scatterSwitch">Scatter</label>
</div>
</label>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Enable 250Hz bandwidth mode</label>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline">
<input
class="form-check-input"
type="checkbox"
id="250HzModeSwitch"
v-model="settings.low_bandwidth_mode"
true-value="True"
false-value="False"
@change="saveSettings"
/>
<label class="form-check-label" for="250HzModeSwitch">250Hz</label>
</div>
</label>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Respond to CQ</label>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline">
<input
class="form-check-input"
type="checkbox"
id="respondCQSwitch"
v-model="settings.respond_to_cq"
true-value="True"
false-value="False"
@change="saveSettings"
/>
<label class="form-check-label" for="respondCQSwitch">QRV</label>
</div>
</label>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">RX buffer size</label>
<label class="input-group-text w-50">
<select
class="form-select form-select-sm"
id="rx_buffer_size"
@change="saveSettings"
v-model="settings.rx_buffer_size"
>
<option value="1">1</option>
<option value="2">2</option>
<option value="4">4</option>
<option value="8">8</option>
<option value="16">16</option>
<option value="32">32</option>
<option value="64">64</option>
<option value="128">128</option>
<option value="256">256</option>
<option value="512">512</option>
<option value="1024">1024</option>
</select>
</label>
</div>
</template>

View file

@ -0,0 +1,53 @@
<script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
function saveSettings() {
saveSettingsToFile();
}
</script>
<template>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Explorer publishing</label>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline">
<input
class="form-check-input"
type="checkbox"
id="ExplorerSwitch"
@change="saveSettings"
v-model="settings.enable_explorer"
true-value="True"
false-value="False"
/>
<label class="form-check-label" for="ExplorerSwitch">Publish</label>
</div>
</label>
</div>
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Explorer stats publishing</label>
<label class="input-group-text w-50">
<div class="form-check form-switch form-check-inline">
<input
class="form-check-input"
type="checkbox"
id="ExplorerStatsSwitch"
@change="saveSettings"
v-model="settings.explorer_stats"
true-value="True"
false-value="False"
/>
<label class="form-check-label" for="ExplorerStatsSwitch"
>Publish stats</label
>
</div>
</label>
</div>
</template>

View file

@ -1,285 +0,0 @@
<!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>Send & Receive Data</title>
</head>
<body>
<div class="container-fluid">
<div class="container mt-1">
<div class="row mb-1">
<div class="col">
<div class="card text-dark bg-light mb-0">
<div class="card-header">Select data</div>
<div class="card-body">
<div class="input-group input-group-sm mb-0">
<input
type="file"
class="form-control"
id="inputGroupFile02"
/>
<label class="input-group-text" for="inputGroupFile02"
>kB</label
>
</div>
</div>
</div>
</div>
<!--col-->
</div>
<!--row-->
<div class="row mb-2">
<div class="col">
<div class="card text-dark bg-light mb-0">
<div class="card-header">Transmission</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-auto">
<div class="input-group input-group-sm">
<input
type="text"
class="form-control"
style="max-width: 6rem"
placeholder="DX Call"
id="dxCall"
maxlength="6"
aria-label="Input group example"
aria-describedby="btnGroupAddon"
/>
<button
type="button"
id="sendPing"
class="btn btn-primary"
>
Ping
</button>
<span class="input-group-text" id="tnc_running_state"
>ACK</span
>
<span class="input-group-text" id="tnc_running_state"
>0000 km</span
>
<span class="input-group-text" id="tnc_running_state"
>0 dB</span
>
</div>
</div>
</div>
<div class="row">
<div class="col-auto">
<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 example"
id="hamlib_deviceport"
>
<option selected value="DATAC1">DATAC1</option>
<option value="DATAC3">DATAC3</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 example"
id="hamlib_deviceport"
>
<option selected value="1">1</option>
<option value="2">2</option>
</select>
</div>
</div>
<div class="col">
<div class="input-group input-group-sm">
<button
type="button"
id="startTransmission"
class="btn btn-success"
>
Send
</button>
<!--<button type="button" id="stopTNC"class="btn btn-danger">STOP</button>-->
</div>
</div>
</div>
</div>
</div>
<!--col-->
</div>
<!--row-->
</div>
<div class="row">
<div class="col">
<div class="card text-dark bg-light mb-0">
<div class="card-header">Info</div>
<div class="card-body">123</div>
</div>
</div>
</div>
<div class="row">
<div class="col">
<hr />
</div>
</div>
<!--row-->
</div>
<!--container-->
</div>
<!---------------------------------------------------------------------- FOOTER AREA ------------------------------------------------------------>
<nav class="navbar fixed-bottom navbar-light bg-light">
<div class="container-fluid">
<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="First group"
>
<button class="btn btn-secondary" id="ptt_state" type="button">
<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>
</div>
<div
class="btn-group btn-group-sm me-2"
role="group"
aria-label="Second group"
>
<button class="btn btn-secondary" id="busy_state" type="button">
<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>
</div>
<div
class="btn-group btn-group-sm me-2"
role="group"
aria-label="Second group"
>
<button class="btn btn-secondary" id="arq_state" type="button">
<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>
</div>
<div
class="btn-group btn-group-sm me-2"
role="group"
aria-label="Third group"
>
<button
class="btn btn-secondary"
id="signalling_state"
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-journal-code"
viewBox="0 0 16 16"
>
<path
fill-rule="evenodd"
d="M8.646 5.646a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708-.708L10.293 8 8.646 6.354a.5.5 0 0 1 0-.708zm-1.292 0a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0 0 .708l2 2a.5.5 0 0 0 .708-.708L5.707 8l1.647-1.646a.5.5 0 0 0 0-.708z"
/>
<path
d="M3 0h10a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-1h1v1a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v1H1V2a2 2 0 0 1 2-2z"
/>
<path
d="M1 5v-.5a.5.5 0 0 1 1 0V5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 3v-.5a.5.5 0 0 1 1 0V8h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 3v-.5a.5.5 0 0 1 1 0v.5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1z"
/>
</svg>
</button>
<button class="btn btn-secondary" id="data_state" type="button">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-journal-richtext"
viewBox="0 0 16 16"
>
<path
d="M7.5 3.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm-.861 1.542 1.33.886 1.854-1.855a.25.25 0 0 1 .289-.047L11 4.75V7a.5.5 0 0 1-.5.5h-5A.5.5 0 0 1 5 7v-.5s1.54-1.274 1.639-1.208zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"
/>
<path
d="M3 0h10a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2v-1h1v1a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v1H1V2a2 2 0 0 1 2-2z"
/>
<path
d="M1 5v-.5a.5.5 0 0 1 1 0V5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 3v-.5a.5.5 0 0 1 1 0V8h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1zm0 3v-.5a.5.5 0 0 1 1 0v.5h.5a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1H1z"
/>
</svg>
</button>
</div>
<div class="input-group input-group-sm me-2">
<span class="input-group-text" id="basic-addon1">Bytes/s</span>
<span class="input-group-text" id="basic-addon1">----</span>
</div>
<div class="progress" style="height: 100%; width: 200px">
<div
class="progress-bar progress-bar-striped bg-primary"
id="arq-progress"
role="progressbar"
style="width: 25%"
aria-valuenow="25"
aria-valuemin="0"
aria-valuemax="100"
>
25%
</div>
</div>
</div>
</div>
</nav>
<script src="../node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View file

@ -1,261 +0,0 @@
<option value="1">Hamlib Dummy</option>
<option value="2">Hamlib NET rigctl</option>
<option value="4">FLRig FLRig</option>
<option value="5">TRXManager TRXManager 5.7.630+</option>
<option value="6">Hamlib Dummy No VFO</option>
<option value="1001">Yaesu FT-847</option>
<option value="1003">Yaesu FT-1000D</option>
<option value="1004">Yaesu MARK-V FT-1000MP</option>
<option value="1005">Yaesu FT-747GX</option>
<option value="1006">Yaesu FT-757GX</option>
<option value="1007">Yaesu FT-757GXII</option>
<option value="1009">Yaesu FT-767GX</option>
<option value="1010">Yaesu FT-736R</option>
<option value="1011">Yaesu FT-840</option>
<option value="1013">Yaesu FT-900</option>
<option value="1014">Yaesu FT-920</option>
<option value="1015">Yaesu FT-890</option>
<option value="1016">Yaesu FT-990</option>
<option value="1017">Yaesu FRG-100</option>
<option value="1018">Yaesu FRG-9600</option>
<option value="1019">Yaesu FRG-8800</option>
<option value="1020">Yaesu FT-817</option>
<option value="1021">Yaesu FT-100</option>
<option value="1022">Yaesu FT-857</option>
<option value="1023">Yaesu FT-897</option>
<option value="1024">Yaesu FT-1000MP</option>
<option value="1025">Yaesu MARK-V Field FT-1000MP</option>
<option value="1026">Yaesu VR-5000</option>
<option value="1027">Yaesu FT-450</option>
<option value="1028">Yaesu FT-950</option>
<option value="1029">Yaesu FT-2000</option>
<option value="1030">Yaesu FTDX-9000</option>
<option value="1031">Yaesu FT-980</option>
<option value="1032">Yaesu FTDX-5000</option>
<option value="1033">Vertex Standard VX-1700</option>
<option value="1034">Yaesu FTDX-1200</option>
<option value="1035">Yaesu FT-991</option>
<option value="1036">Yaesu FT-891</option>
<option value="1037">Yaesu FTDX-3000</option>
<option value="1038">Yaesu FT-847UNI</option>
<option value="1039">Yaesu FT-600</option>
<option value="1040">Yaesu FTDX-101D</option>
<option value="1041">Yaesu FT-818</option>
<option value="1042">Yaesu FTDX-10</option>
<option value="1043">Yaesu FT-897D</option>
<option value="1044">Yaesu FTDX-101MP</option>
<option value="2001">Kenwood TS-50S</option>
<option value="2002">Kenwood TS-440S</option>
<option value="2003">Kenwood TS-450S</option>
<option value="2004">Kenwood TS-570D</option>
<option value="2005">Kenwood TS-690S</option>
<option value="2006">Kenwood TS-711</option>
<option value="2007">Kenwood TS-790</option>
<option value="2008">Kenwood TS-811</option>
<option value="2009">Kenwood TS-850</option>
<option value="2010">Kenwood TS-870S</option>
<option value="2011">Kenwood TS-940S</option>
<option value="2012">Kenwood TS-950S</option>
<option value="2013">Kenwood TS-950SDX</option>
<option value="2014">Kenwood TS-2000</option>
<option value="2015">Kenwood R-5000</option>
<option value="2016">Kenwood TS-570S</option>
<option value="2017">Kenwood TH-D7A</option>
<option value="2019">Kenwood TH-F6A</option>
<option value="2020">Kenwood TH-F7E</option>
<option value="2021">Elecraft K2</option>
<option value="2022">Kenwood TS-930</option>
<option value="2023">Kenwood TH-G71</option>
<option value="2024">Kenwood TS-680S</option>
<option value="2025">Kenwood TS-140S</option>
<option value="2026">Kenwood TM-D700</option>
<option value="2027">Kenwood TM-V7</option>
<option value="2028">Kenwood TS-480</option>
<option value="2029">Elecraft K3</option>
<option value="2030">Kenwood TRC-80</option>
<option value="2031">Kenwood TS-590S</option>
<option value="2032">SigFox Transfox</option>
<option value="2033">Kenwood TH-D72A</option>
<option value="2034">Kenwood TM-D710(G)</option>
<option value="2036">FlexRadio 6xxx</option>
<option value="2037">Kenwood TS-590SG</option>
<option value="2038">Elecraft XG3</option>
<option value="2039">Kenwood TS-990s</option>
<option value="2040">OpenHPSDR PiHPSDR</option>
<option value="2041">Kenwood TS-890S</option>
<option value="2042">Kenwood TH-D74</option>
<option value="2043">Elecraft K3S</option>
<option value="2044">Elecraft KX2</option>
<option value="2045">Elecraft KX3</option>
<option value="2046">Hilberling PT-8000A</option>
<option value="2047">Elecraft K4</option>
<option value="2048">FlexRadio/ANAN PowerSDR/Thetis</option>
<option value="2049">Malachite DSP</option>
<option value="3002">Icom IC-1275</option>
<option value="3003">Icom IC-271</option>
<option value="3004">Icom IC-275</option>
<option value="3006">Icom IC-471</option>
<option value="3007">Icom IC-475</option>
<option value="3009">Icom IC-706</option>
<option value="3010">Icom IC-706MkII</option>
<option value="3011">Icom IC-706MkIIG</option>
<option value="3012">Icom IC-707</option>
<option value="3013">Icom IC-718</option>
<option value="3014">Icom IC-725</option>
<option value="3015">Icom IC-726</option>
<option value="3016">Icom IC-728</option>
<option value="3017">Icom IC-729</option>
<option value="3019">Icom IC-735</option>
<option value="3020">Icom IC-736</option>
<option value="3021">Icom IC-737</option>
<option value="3022">Icom IC-738</option>
<option value="3023">Icom IC-746</option>
<option value="3024">Icom IC-751</option>
<option value="3026">Icom IC-756</option>
<option value="3027">Icom IC-756PRO</option>
<option value="3028">Icom IC-761</option>
<option value="3029">Icom IC-765</option>
<option value="3030">Icom IC-775</option>
<option value="3031">Icom IC-781</option>
<option value="3032">Icom IC-820H</option>
<option value="3034">Icom IC-821H</option>
<option value="3035">Icom IC-970</option>
<option value="3036">Icom IC-R10</option>
<option value="3037">Icom IC-R71</option>
<option value="3038">Icom IC-R72</option>
<option value="3039">Icom IC-R75</option>
<option value="3040">Icom IC-R7000</option>
<option value="3041">Icom IC-R7100</option>
<option value="3042">Icom ICR-8500</option>
<option value="3043">Icom IC-R9000</option>
<option value="3044">Icom IC-910</option>
<option value="3045">Icom IC-78</option>
<option value="3046">Icom IC-746PRO</option>
<option value="3047">Icom IC-756PROII</option>
<option value="3051">Ten-Tec Omni VI Plus</option>
<option value="3052">Optoelectronics OptoScan535</option>
<option value="3053">Optoelectronics OptoScan456</option>
<option value="3054">Icom IC ID-1</option>
<option value="3055">Icom IC-703</option>
<option value="3056">Icom IC-7800</option>
<option value="3057">Icom IC-756PROIII</option>
<option value="3058">Icom IC-R20</option>
<option value="3060">Icom IC-7000</option>
<option value="3061">Icom IC-7200</option>
<option value="3062">Icom IC-7700</option>
<option value="3063">Icom IC-7600</option>
<option value="3064">Ten-Tec Delta II</option>
<option value="3065">Icom IC-92D</option>
<option value="3066">Icom IC-R9500</option>
<option value="3067">Icom IC-7410</option>
<option value="3068">Icom IC-9100</option>
<option value="3069">Icom IC-RX7</option>
<option value="3070">Icom IC-7100</option>
<option value="3071">Icom ID-5100</option>
<option value="3072">Icom IC-2730</option>
<option value="3073">Icom IC-7300</option>
<option value="3074">Microtelecom Perseus</option>
<option value="3075">Icom IC-785x</option>
<option value="3076">Xeigu X108G</option>
<option value="3077">Icom IC-R6</option>
<option value="3078">Icom IC-7610</option>
<option value="3079">Icom IC-R8600</option>
<option value="3080">Icom IC-R30</option>
<option value="3081">Icom IC-9700</option>
<option value="3082">Icom ID-4100</option>
<option value="3083">Icom ID-31</option>
<option value="3084">Icom ID-51</option>
<option value="3085">Icom IC-705</option>
<option value="4001">Icom IC-PCR1000</option>
<option value="4002">Icom IC-PCR100</option>
<option value="4003">Icom IC-PCR1500</option>
<option value="4004">Icom IC-PCR2500</option>
<option value="5001">AOR AR8200</option>
<option value="5002">AOR AR8000</option>
<option value="5003">AOR AR7030</option>
<option value="5004">AOR AR5000</option>
<option value="5005">AOR AR3030</option>
<option value="5006">AOR AR3000A</option>
<option value="5008">AOR AR2700</option>
<option value="5013">AOR AR8600</option>
<option value="5014">AOR AR5000A</option>
<option value="5015">AOR AR7030 Plus</option>
<option value="5016">AOR SR2200</option>
<option value="6005">JRC NRD-525</option>
<option value="6006">JRC NRD-535D</option>
<option value="6007">JRC NRD-545 DSP</option>
<option value="8001">Uniden BC780xlt</option>
<option value="8002">Uniden BC245xlt</option>
<option value="8003">Uniden BC895xlt</option>
<option value="8004">Radio Shack PRO-2052</option>
<option value="8006">Uniden BC250D</option>
<option value="8010">Uniden BCD-396T</option>
<option value="8011">Uniden BCD-996T</option>
<option value="8012">Uniden BC898T</option>
<option value="9002">Drake R-8A</option>
<option value="9003">Drake R-8B</option>
<option value="10004">Lowe HF-235</option>
<option value="11003">Racal RA6790/GM</option>
<option value="11005">Racal RA3702</option>
<option value="12004">Watkins-Johnson WJ-8888</option>
<option value="14002">Skanti TRP8000</option>
<option value="14004">Skanti TRP 8255 S R</option>
<option value="15001">Winradio WR-1000</option>
<option value="15002">Winradio WR-1500</option>
<option value="15003">Winradio WR-1550</option>
<option value="15004">Winradio WR-3100</option>
<option value="15005">Winradio WR-3150</option>
<option value="15006">Winradio WR-3500</option>
<option value="15007">Winradio WR-3700</option>
<option value="15009">Winradio WR-G313</option>
<option value="16001">Ten-Tec TT-550</option>
<option value="16002">Ten-Tec TT-538 Jupiter</option>
<option value="16003">Ten-Tec RX-320</option>
<option value="16004">Ten-Tec RX-340</option>
<option value="16005">Ten-Tec RX-350</option>
<option value="16007">Ten-Tec TT-516 Argonaut V</option>
<option value="16008">Ten-Tec TT-565 Orion</option>
<option value="16009">Ten-Tec TT-585 Paragon</option>
<option value="16011">Ten-Tec TT-588 Omni VII</option>
<option value="16012">Ten-Tec RX-331</option>
<option value="16013">Ten-Tec TT-599 Eagle</option>
<option value="17001">Alinco DX-77</option>
<option value="17002">Alinco DX-SR8</option>
<option value="18001">Kachina 505DSP</option>
<option value="22001">TAPR DSP-10</option>
<option value="23001">Flex-radio SDR-1000</option>
<option value="23003">DTTS Microwave Society DttSP IPC</option>
<option value="23004">DTTS Microwave Society DttSP UDP</option>
<option value="24001">RFT EKD-500</option>
<option value="25001">Elektor Elektor 3/04</option>
<option value="25002">SAT-Schneider DRT1</option>
<option value="25003">Coding Technologies Digital World Traveller</option>
<option value="25006">AmQRP DDS-60</option>
<option value="25007">Elektor Elektor SDR-USB</option>
<option value="25008">mRS miniVNA</option>
<option value="25009">SoftRock Si570 AVR-USB</option>
<option value="25011">KTH-SDR kit Si570 PIC-USB</option>
<option value="25012">FiFi FiFi-SDR</option>
<option value="25013">AMSAT-UK FUNcube Dongle</option>
<option value="25014">N2ADR HiQSDR</option>
<option value="25015">Funkamateur FA-SDR</option>
<option value="25016">AE9RB Si570 Peaberry V1</option>
<option value="25017">AE9RB Si570 Peaberry V2</option>
<option value="25018">AMSAT-UK FUNcube Dongle Pro+</option>
<option value="25019">HobbyPCB RS-HFIQ</option>
<option value="26001">Video4Linux SW/FM radio</option>
<option value="26002">Video4Linux2 SW/FM radio</option>
<option value="27001">Rohde&Schwarz ESMC</option>
<option value="27002">Rohde&Schwarz EB200</option>
<option value="27003">Rohde&Schwarz XK2100</option>
<option value="28001">Philips/Simoco PRM8060</option>
<option value="29001">ADAT www.adat.ch ADT-200A</option>
<option value="30001">Icom IC-M700PRO</option>
<option value="30002">Icom IC-M802</option>
<option value="30003">Icom IC-M710</option>
<option value="30004">Icom IC-M803</option>
<option value="31001">Dorji DRA818V</option>
<option value="31002">Dorji DRA818U</option>
<option value="32001">Barrett 2050</option>
<option value="32002">Barrett 950</option>
<option value="33001">ELAD FDM-DUO</option>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 590 KiB

File diff suppressed because it is too large Load diff

1006
gui/src/js/chatHandler.ts Normal file

File diff suppressed because it is too large Load diff

281
gui/src/js/daemon.ts Normal file
View file

@ -0,0 +1,281 @@
//var net = require("net");
var net = require("node:net");
// ----------------- init pinia stores -------------
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useAudioStore } from "../store/audioStore.js";
const audioStore = useAudioStore(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
var daemon = new net.Socket();
var socketchunk = []; // Current message, per connection.
// global to keep track of daemon connection error emissions
var daemonShowConnectStateError = 1;
setTimeout(connectDAEMON, 500);
function connectDAEMON() {
if (daemonShowConnectStateError == 1) {
console.log("connecting to daemon");
}
//clear message buffer after reconnecting or initial connection
socketchunk = [];
daemon.connect(settings.daemon_port, settings.daemon_host);
//client.setTimeout(5000);
}
daemon.on("connect", function () {
console.log("daemon connection established");
daemonShowConnectStateError = 1;
});
daemon.on("error", function (err) {
if (daemonShowConnectStateError == 1) {
console.log("daemon connection error");
console.log("Make sure the daemon is started.");
console.log('Run "python daemon.py" in the modem directory.');
console.log(err);
daemonShowConnectStateError = 0;
}
setTimeout(connectDAEMON, 500);
daemon.destroy();
});
/*
client.on('close', function(data) {
console.log(' Modem connection closed');
setTimeout(connectModem, 2000)
let Data = {
daemon_connection: daemon.readyState,
};
ipcRenderer.send('request-update-daemon-connection', Data);
});
*/
daemon.on("end", function (data) {
console.log("daemon connection ended");
console.log(data);
daemon.destroy();
setTimeout(connectDAEMON, 500);
});
//exports.writeDaemonCommand = function(command){
//writeDaemonCommand = function (command) {
function writeDaemonCommand(command) {
// we use the writingCommand function to update our TCPIP state because we are calling this function a lot
// if socket opened, we are able to run commands
if (daemon.readyState == "open") {
//uiMain.setDAEMONconnection('open')
daemon.write(command + "\n");
}
if (daemon.readyState == "closed") {
//uiMain.setDAEMONconnection('closed')
}
if (daemon.readyState == "opening") {
//uiMain.setDAEMONconnection('opening')
}
}
// "https://stackoverflow.com/questions/9070700/nodejs-net-createserver-large-amount-of-data-coming-in"
daemon.on("data", function (socketdata) {
/*
inspired by:
stackoverflow.com questions 9070700 nodejs-net-createserver-large-amount-of-data-coming-in
*/
socketdata = socketchunk.join("\n") + socketdata.toString("utf8"); //append incoming data to socketchunk
//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 (socketdata.startsWith('{"') && socketdata.endsWith('"}\n')) {
var data = "";
// split data into chunks if we received multiple commands
socketchunk = socketdata.split("\n");
data = JSON.parse(socketchunk[0]);
// search for empty entries in socketchunk and remove them
for (var i = 0; i < socketchunk.length; i++) {
if (socketchunk[i] === "") {
socketchunk.splice(i, 1);
}
}
//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.debug(socketchunk[i]);
socketchunk = [];
}
}
console.log(data);
if (data["command"] == "daemon_state") {
// update audio devices by putting them to audio store
audioStore.inputDevices = data["input_devices"];
audioStore.outputDevices = data["output_devices"];
settings.serial_devices = data["serial_devices"];
state.python_version = data["python_version"];
state.modem_version = data["version"];
state.modem_running_state = data["daemon_state"][0]["status"];
state.rigctld_started = data["rigctld_state"][0]["status"];
//state.rigctld_process = data["daemon_state"][0]["rigctld_process"];
}
if (data["command"] == "test_hamlib") {
//
}
}
//finally delete message buffer
socketchunk = [];
}
});
// START Modem
// ` `== multi line string
export function startModem() {
var json_command = JSON.stringify({
type: "set",
command: "start_modem",
parameter: [
{
mycall: settings.mycall,
mygrid: settings.mygrid,
rx_audio: audioStore.startupInputDevice,
tx_audio: audioStore.startupOutputDevice,
radiocontrol: settings.radiocontrol,
devicename: settings.hamlib_deviceid,
deviceport: settings.hamlib_deviceport,
pttprotocol: settings.hamlib_pttprotocol,
pttport: settings.hamlib_ptt_port,
serialspeed: settings.hamlib_serialspeed,
data_bits: settings.hamlib_data_bits,
stop_bits: settings.hamlib_stop_bits,
handshake: settings.hamlib_handshake,
rigctld_port: settings.hamlib_rigctld_port,
rigctld_ip: settings.hamlib_rigctld_ip,
enable_scatter: settings.enable_scatter,
enable_fft: settings.enable_fft,
enable_fsk: settings.enable_fsk,
low_bandwidth_mode: settings.low_bandwidth_mode,
tuning_range_fmin: settings.tuning_range_fmin,
tuning_range_fmax: settings.tuning_range_fmax,
//tx_audio_level: settings.tx_audio_level,
respond_to_cq: settings.respond_to_cq,
rx_buffer_size: settings.rx_buffer_size,
enable_explorer: settings.enable_explorer,
enable_stats: settings.explorer_stats,
enable_auto_tune: settings.auto_tune,
tx_delay: settings.tx_delay,
tci_ip: settings.tci_ip,
tci_port: settings.tci_port,
enable_mesh: settings.enable_mesh_features,
},
],
});
console.log(json_command);
writeDaemonCommand(json_command);
}
// STOP Modem
//exports.stopModem = function () {
export function stopModem() {
var command =
'{"type" : "set", "command": "stop_modem" , "parameter": "---" }';
writeDaemonCommand(command);
}
// TEST HAMLIB
function testHamlib(
//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",
parameter: [
{
radiocontrol: radiocontrol,
devicename: devicename,
deviceport: deviceport,
pttprotocol: pttprotocol,
pttport: pttport,
serialspeed: serialspeed,
data_bits: data_bits,
stop_bits: stop_bits,
handshake: handshake,
rigctld_port: rigctld_port,
rigctld_ip: rigctld_ip,
},
],
});
console.log(json_command);
writeDaemonCommand(json_command);
}
export function startRigctld() {
var json_command = JSON.stringify({
type: "set",
command: "start_rigctld",
parameter: [
{
hamlib_deviceid: settings.hamlib_deviceid,
hamlib_deviceport: settings.hamlib_deviceport,
hamlib_stop_bits: settings.hamlib_stop_bits,
hamlib_data_bits: settings.hamlib_data_bits,
hamlib_handshake: settings.hamlib_handshake,
hamlib_serialspeed: settings.hamlib_serialspeed,
hamlib_dtrstate: settings.hamlib_dtrstate,
hamlib_pttprotocol: settings.hamlib_pttprotocol,
hamlib_ptt_port: settings.hamlib_ptt_port,
hamlib_dcd: settings.hamlib_dcd,
hamlbib_serialspeed_ptt: settings.hamlib_serialspeed,
hamlib_rigctld_port: settings.hamlib_rigctld_port,
hamlib_rigctld_ip: settings.hamlib_rigctld_ip,
hamlib_rigctld_path: settings.hamlib_rigctld_path,
hamlib_rigctld_server_port: settings.hamlib_rigctld_server_port,
hamlib_rigctld_custom_args: settings.hamlib_rigctld_custom_args,
},
],
});
console.log(json_command);
writeDaemonCommand(json_command);
}
export function stopRigctld() {
let command = '{"type" : "set", "command": "stop_rigctld"}';
writeDaemonCommand(command);
}

View file

@ -37,7 +37,7 @@ const dateFormatHours = new Intl.DateTimeFormat(navigator.language, {
hour12: false,
});
// split character
const split_char = "\0;\1;";
//const split_char = "\0;\1;";
// global for our selected file we want to transmit
// ----------------- some chat globals
var filetype = "";
@ -80,6 +80,8 @@ PouchDB.plugin(require("pouchdb-find"));
//PouchDB.plugin(require('pouchdb-replication'));
PouchDB.plugin(require("pouchdb-upsert"));
var db = new PouchDB(chatDB);
var users = new PouchDB(userDB);
@ -150,7 +152,8 @@ var chatFilter = [
window.addEventListener("DOMContentLoaded", () => {
updateAllChat(false);
// theme selector
changeGuiDesign(config.theme);
// TODO: Remove for one pager, also remove function!
//changeGuiDesign(config.theme);
const userInfoFields = [
"user_info_image",
@ -276,7 +279,8 @@ window.addEventListener("DOMContentLoaded", () => {
.then(function (doc) {
db.remove(doc)
.then(function (doc) {
return location.reload();
updateAllChat(true);
return true;
})
.catch(function (err) {
console.log(err);
@ -343,7 +347,7 @@ window.addEventListener("DOMContentLoaded", () => {
lines = 6;
}
}
var message_container_height_offset = 130 + 20 * lines;
var message_container_height_offset = 180 + 20 * lines;
var message_container_height = `calc(100% - ${message_container_height_offset}px)`;
document.getElementById("message-container").style.height =
message_container_height;
@ -373,7 +377,7 @@ window.addEventListener("DOMContentLoaded", () => {
"bi bi-chevron-compact-up";
}
var message_container_height_offset = 130 + 20 * lines;
var message_container_height_offset = 180 + 20 * lines;
//var message_container_height_offset = 90 + (23*lines);
var message_container_height = `calc(100% - ${message_container_height_offset}px)`;
document.getElementById("message-container").style.height =
@ -456,7 +460,7 @@ window.addEventListener("DOMContentLoaded", () => {
//Remove non-printable chars from begining and end of string--should save us a byte here and there
chatmessage = chatmessage.toString().trim();
// reset textarea size
var message_container_height_offset = 150;
var message_container_height_offset = 200;
var message_container_height = `calc(100% - ${message_container_height_offset}px)`;
document.getElementById("message-container").style.height =
message_container_height;
@ -669,24 +673,12 @@ ipcRenderer.on("return-select-user-image", (event, arg) => {
});
});
ipcRenderer.on("action-update-reception-status", (event, arg) => {
var data = arg["data"][0];
document.getElementById("txtConnectedWithChat").textContent = data.dxcallsign;
});
ipcRenderer.on("action-clear-reception-status", (event) => {
//Clear connected with textbox
let cwc = document.getElementById("txtConnectedWithChat");
if (cwc.textContent != "------") {
cwc.textContent = "------";
//console.log("Reseting connected with");
}
});
ipcRenderer.on("action-update-transmission-status", (event, arg) => {
var data = arg["data"][0];
document.getElementById("txtConnectedWithChat").textContent = data.dxcallsign;
if (data.status == "opening") return;
if (typeof data.uuid === undefined) return;
@ -1064,6 +1056,7 @@ update_chat = function (obj) {
var attempt = obj.attempt;
}
// add percent and bytes per minute if not existing
//console.log(obj.percent)
if (typeof obj.percent == "undefined") {
@ -1266,13 +1259,13 @@ update_chat = function (obj) {
`;
document
.getElementById("list-tab")
.getElementById("list-tab-chat")
.insertAdjacentHTML("beforeend", new_callsign);
var message_area = `
<div class="tab-pane fade ${callsign_selected}" id="chat-${dxcallsign}" role="tabpanel" aria-labelledby="chat-${dxcallsign}-list"></div>
`;
document
.getElementById("nav-tabContent")
.getElementById("nav-tabContent-Chat")
.insertAdjacentHTML("beforeend", message_area);
// finally get and set user information to first selected item
@ -1392,36 +1385,40 @@ update_chat = function (obj) {
showOsPopUp("Message received from " + obj.dxcallsign, obj.msg);
}
// check if message is signed or not for adjusting icon
if (
typeof obj.hmac_signed !== "undefined" &&
obj.hmac_signed !== "False"
) {
console.log(hmac_signed);
var hmac_signed = '<i class="bi bi-shield-fill-check"></i>';
} else {
var hmac_signed = '<i class="bi bi-shield-x"></i>';
}
// check if message is signed or not for adjusting icon
if(typeof obj.hmac_signed !== "undefined" && obj.hmac_signed !== "False"){
console.log(hmac_signed)
var hmac_signed = '<i class="bi bi-shield-fill-check"></i>';
} else {
var hmac_signed = '<i class="bi bi-shield-x"></i>';
}
var new_message = `
<div class="d-flex align-items-center" style="margin-left: auto;"> <!-- max-width: 75%; -->
<div class="mt-3 rounded-3 mb-0" style="max-width: 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 border-light bg-body-tertiary" id="msg-${obj._id}">
${fileheader}
<div class="card-body rounded-3 p-0">
<p class="card-text p-2 mb-0 text-break text-wrap">${message_html}</p>
<p class="text-right mb-0 p-1 text-white" style="text-align: left; font-size : 0.9rem">
<span class="badge bg-light text-muted">${timestamp}</span>
<span class="badge bg-body-tertiary text-muted">${timestamp}</span>
</p>
<span id="msg-${obj._id}-hmac-badge" class="position-absolute top-0 start-100 translate-middle badge rounded-1 bg-secondary border border-white">
<span id="msg-${
obj._id
}-hmac-badge" class="position-absolute top-0 start-100 translate-middle badge rounded-1 bg-secondary border border-white">
<span id="msg-${obj._id}-hmac-signed" class="">${hmac_signed}</span>
<span id="msg-${
obj._id
}-hmac-signed" class="">${hmac_signed}</span>
</span>
</div>
@ -1438,11 +1435,11 @@ update_chat = function (obj) {
<div class="d-flex align-items-center" style="margin-left: auto;"> <!-- max-width: 75%; -->
<div class="mt-3 rounded-3 mb-0" style="max-width: 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 border-light bg-body-tertiary" id="msg-${obj._id}">
<div class="card-body rounded-3 p-0">
<p class="card-text p-2 mb-0 text-break text-wrap">${message_html}</p>
<p class="text-right mb-0 p-1 text-white" style="text-align: left; font-size : 0.9rem">
<span class="badge bg-light text-muted">${timestamp}</span>
<span class="badge bg-body-tertiary text-muted">${timestamp}</span>
</p>
@ -2252,9 +2249,9 @@ async function updateAllChat(clear) {
callsign_counter = 0;
//selected_callsign = "";
dxcallsigns.clear();
document.getElementById("list-tab").innerHTML = "";
document.getElementById("nav-tabContent").innerHTML = "";
//document.getElementById("list-tab").childNodes.remove();
document.getElementById("list-tab-chat").innerHTML = "";
document.getElementById("nav-tabContent-Chat").innerHTML = "";
//document.getElementById("list-tab-chat").childNodes.remove();
//document.getElementById("nav-tab-content").childrenNodes.remove();
}
//Ensure we create an index before running db.find
@ -2905,6 +2902,8 @@ function clearUnreadMessages(dxcall) {
.catch(function (err) {
console.log(err);
});
}
//Have the operating system show a notification popup

View file

@ -1,8 +1,23 @@
const path = require("path");
const { ipcRenderer, shell, clipboard } = require("electron");
const { shell, clipboard } = require("electron");
const exec = require("child_process").spawn;
const sock = require("./sock.js");
const daemon = require("./daemon.js");
//import { useIpcRenderer } from '@vueuse/electron'
//const ipcRenderer = useIpcRenderer()
//import * as bootstrap from 'bootstrap'
//require("./preload-chat.js");
//require("./preload-mesh.js");
//require("./preload-log.js");
//const daemon = require("./daemon.js");
//const sock = require("./sock.js");
const fs = require("fs");
const FD = require("./freedata");
const {
@ -398,12 +413,12 @@ window.addEventListener("DOMContentLoaded", () => {
}
if (config.enable_mesh_features.toLowerCase() == "true") {
document.getElementById("liMeshTable").style.visibility = "visible";
document.getElementById("liMeshTable").style.display = "block";
document.getElementById("list-mesh-list").style.visibility = "visible";
document.getElementById("list-mesh-list").style.display = "block";
document.getElementById("enableMeshSwitch").checked = true;
} else {
document.getElementById("liMeshTable").style.visibility = "hidden";
document.getElementById("liMeshTable").style.display = "none";
document.getElementById("list-mesh-list").style.visibility = "hidden";
document.getElementById("list-mesh-list").style.display = "none";
document.getElementById("enableMeshSwitch").checked = false;
}
@ -560,7 +575,7 @@ window.addEventListener("DOMContentLoaded", () => {
document.getElementById("wftheme_selector").value = config.wftheme;
spectrum.setColorMap(config.wftheme);
document.getElementById("btnAbout").addEventListener("click", () => {
document.getElementById("list-info-list").addEventListener("click", () => {
document.getElementById("aboutVersion").innerText = appVer;
let maxcol = 3;
let col = 2;
@ -1258,12 +1273,12 @@ window.addEventListener("DOMContentLoaded", () => {
document.getElementById("enableMeshSwitch").addEventListener("click", () => {
if (document.getElementById("enableMeshSwitch").checked == true) {
config.enable_mesh_features = "True";
document.getElementById("liMeshTable").style.visibility = "visible";
document.getElementById("liMeshTable").style.display = "block";
document.getElementById("list-mesh-list").style.visibility = "visible";
document.getElementById("list-mesh-list").style.display = "block";
} else {
config.enable_mesh_features = "False";
document.getElementById("liMeshTable").style.visibility = "hidden";
document.getElementById("liMeshTable").style.display = "none";
document.getElementById("list-mesh-list").style.visibility = "hidden";
document.getElementById("list-mesh-list").style.display = "none";
}
//fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
FD.saveConfig(config, configPath);
@ -1458,12 +1473,12 @@ window.addEventListener("DOMContentLoaded", () => {
if (document.getElementById("enableMeshSwitch").checked == true) {
var enable_mesh_features = "True";
document.getElementById("liMeshTable").style.visibility = "visible";
document.getElementById("liMeshTable").style.display = "block";
document.getElementById("list-mesh-list").style.visibility = "visible";
document.getElementById("list-mesh-list").style.display = "block";
} else {
var enable_mesh_features = "False";
document.getElementById("liMeshTable").style.visibility = "hidden";
document.getElementById("liMeshTable").style.display = "none";
document.getElementById("list-mesh-list").style.visibility = "hidden";
document.getElementById("list-mesh-list").style.display = "none";
}
if (document.getElementById("scatterSwitch").checked == true) {
@ -1634,13 +1649,6 @@ window.addEventListener("DOMContentLoaded", () => {
);
});
document.getElementById("tncLog").addEventListener("click", () => {
ipcRenderer.send("request-open-tnc-log");
});
document.getElementById("meshtable").addEventListener("click", () => {
ipcRenderer.send("request-open-mesh-module");
});
// stopTNC button clicked
document.getElementById("stopTNC").addEventListener("click", () => {
@ -1746,13 +1754,7 @@ window.addEventListener("DOMContentLoaded", () => {
sock.disconnectARQ();
});
// OPEN CHAT MODULE
document.getElementById("openRFChat").addEventListener("click", () => {
let Data = {
command: "openRFChat",
};
ipcRenderer.send("request-show-chat-window", Data);
});
document.getElementById("thTime").addEventListener("click", () => {
if (hslLastSort == 0 && hslLastSortDir == "asc") hslLastSortDir = "desc";
@ -2015,9 +2017,8 @@ ipcRenderer.on("action-update-tnc-state", (event, arg) => {
checkForNewMessageWait = -1;
}
checkForNewMessageWait++;
// update FFT
if (typeof arg.fft !== "undefined") {
// FIXME: WE need to fix this when disabled waterfall chart
// update FFT only if data available
if (typeof arg.fft !== "undefined" && arg.fft !== "[]") {
try {
var array = JSON.parse("[" + arg.fft + "]");
spectrum.addData(array[0]);
@ -2757,7 +2758,6 @@ ipcRenderer.on("action-update-hamlib-test", (event, arg) => {
//document.getElementById("testHamlibAdvanced").className = "btn btn-sm btn-danger";
}
});
ipcRenderer.on("action-update-daemon-connection", (event, arg) => {
if (arg.daemon_connection == "open") {
document.getElementById("daemon_connection_state").className =
@ -2775,88 +2775,6 @@ ipcRenderer.on("action-update-daemon-connection", (event, arg) => {
//document.getElementById("blurdiv").style.webkitFilter = "blur(10px)";
}
});
ipcRenderer.on("action-update-tnc-connection", (event, arg) => {
if (arg.tnc_connection == "open") {
/*
document.getElementById('hamlib_deviceid').disabled = true;
document.getElementById('hamlib_deviceport').disabled = true;
document.getElementById('testHamlib').disabled = true;
document.getElementById('hamlib_ptt_protocol').disabled = true;
document.getElementById('audio_input_selectbox').disabled = true;
document.getElementById('audio_output_selectbox').disabled = true;
//document.getElementById('stopTNC').disabled = false;
document.getElementById('startTNC').disabled = true;
document.getElementById('dxCall').disabled = false;
document.getElementById("hamlib_serialspeed").disabled = true;
document.getElementById("openDataModule").disabled = false;
*/
// collapse settings screen
var collapseFirstRow = new bootstrap.Collapse(
document.getElementById("collapseFirstRow"),
{ toggle: false },
);
collapseFirstRow.hide();
var collapseSecondRow = new bootstrap.Collapse(
document.getElementById("collapseSecondRow"),
{ toggle: false },
);
collapseSecondRow.hide();
var collapseThirdRow = new bootstrap.Collapse(
document.getElementById("collapseThirdRow"),
{ toggle: false },
);
collapseThirdRow.show();
var collapseFourthRow = new bootstrap.Collapse(
document.getElementById("collapseFourthRow"),
{ toggle: false },
);
collapseFourthRow.show();
//Set tuning for fancy graphics mode (high/low CPU)
set_CPU_mode();
//GUI will auto connect to TNC if already running, if that is the case increment start count if 0
if (tncStartCount == 0) tncStartCount++;
} else {
/*
document.getElementById('hamlib_deviceid').disabled = false;
document.getElementById('hamlib_deviceport').disabled = false;
document.getElementById('testHamlib').disabled = false;
document.getElementById('hamlib_ptt_protocol').disabled = false;
document.getElementById('audio_input_selectbox').disabled = false;
document.getElementById('audio_output_selectbox').disabled = false;
//document.getElementById('stopTNC').disabled = true;
document.getElementById('startTNC').disabled = false;
document.getElementById('dxCall').disabled = true;
document.getElementById("hamlib_serialspeed").disabled = false;
document.getElementById("openDataModule").disabled = true;
*/
// collapse settings screen
var collapseFirstRow = new bootstrap.Collapse(
document.getElementById("collapseFirstRow"),
{ toggle: false },
);
collapseFirstRow.show();
var collapseSecondRow = new bootstrap.Collapse(
document.getElementById("collapseSecondRow"),
{ toggle: false },
);
collapseSecondRow.show();
var collapseThirdRow = new bootstrap.Collapse(
document.getElementById("collapseThirdRow"),
{ toggle: false },
);
collapseThirdRow.hide();
var collapseFourthRow = new bootstrap.Collapse(
document.getElementById("collapseFourthRow"),
{ toggle: false },
);
collapseFourthRow.hide();
}
});
ipcRenderer.on("action-update-rx-buffer", (event, arg) => {
var data = arg.data["data"];
@ -2976,11 +2894,11 @@ ipcRenderer.on("run-tnc-command-fec-iswriting", (event) => {
ipcRenderer.on("action-update-unread-messages-main", (event, data) => {
//Do something
if (data == true) {
document.getElementById("openRFChat").classList.add("btn-warning");
document.getElementById("openRFChat").classList.remove("btn-secondary");
document.getElementById("list-messages-list").classList.add("bg-warning");
document.getElementById("list-messages-list").classList.remove("bg-secondary");
} else {
document.getElementById("openRFChat").classList.remove("btn-warning");
document.getElementById("openRFChat").classList.add("btn-secondary");
document.getElementById("list-messages-list").classList.remove("bg-warning");
document.getElementById("list-messages-list").classList.add("bg-secondary");
}
});
@ -3143,7 +3061,6 @@ ipcRenderer.on("action-updater", (event, arg) => {
});
// ----------- INFO MODAL ACTIONS -------------------------------
// CQ TRANSMITTING
ipcRenderer.on("action-show-cq-toast-transmitting", (event, data) => {
displayToast(

View file

@ -1,37 +1,27 @@
const fs = require("fs");
const { ipcRenderer } = require("electron");
/**
* Save config and update config setting globally
* @param {string} config - config data
* @param {string} configPath
*/
exports.saveConfig = function (config, configPath) {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
ipcRenderer.send("set-config-global", config);
};
/**
* Binary to ASCII replacement
* @param {string} data in normal/usual utf-8 format
* @returns base64 encoded string
*/
exports.btoa_FD = function (data) {
export function btoa_FD(data) {
//exports.btoa_FD = function (data) {
return Buffer.from(data, "utf-8").toString("base64");
};
}
/**
* ASCII to Binary replacement
* @param {string} data in base64 encoding
* @returns utf-8 normal/usual string
*/
exports.atob_FD = function (data) {
export function atob_FD(data) {
//exports.atob_FD = function (data) {
return Buffer.from(data, "base64").toString("utf-8");
};
}
/**
* UTF8 to ASCII btoa
* @param {string} data in base64 encoding
* @returns base64 bota compatible data for use in browser
*/
exports.atob = function (data) {
export function atob(data) {
//exports.atob = function (data) {
return window.btoa(Buffer.from(data, "base64").toString("utf8"));
};
}

View file

@ -0,0 +1,44 @@
const { v4: uuidv4 } = require("uuid");
import * as bootstrap from "bootstrap";
export function displayToast(type, icon, content, duration) {
let mainToastContainer = document.getElementById("mainToastContainer");
let randomID = uuidv4();
let toastCode = `
<div class="toast align-items-center bg-outline-${type} border-1" id="${randomID}" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body p-0 m-0 bg-white rounded-2 w-100">
<div class="row p-1 m-0">
<div class="col-auto bg-${type} rounded-start rounded-2 d-flex align-items-center">
<i class="bi ${icon}" style="font-size: 1rem; color: white"></i>
</div>
<div class="col p-2">
${content}
</div>
</div>
</div>
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
`;
// insert toast to toast container
mainToastContainer.insertAdjacentHTML("beforeend", toastCode);
// register toast
let toastHTMLElement = document.getElementById(randomID);
let toast = bootstrap.Toast.getOrCreateInstance(toastHTMLElement); // Returns a Bootstrap toast instance
toast._config.delay = duration;
// show toast
toast.show();
//register event listener if toast is hidden
toastHTMLElement.addEventListener("hidden.bs.toast", () => {
// remove eventListener
toastHTMLElement.removeEventListener("hidden.bs.toast", this);
// remove toast
toastHTMLElement.remove();
});
}

View file

@ -0,0 +1,152 @@
import path from "node:path";
import fs from "fs";
// pinia store setup
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
// ---------------------------------
console.log(process.env);
var appDataFolder = "undefined";
if (typeof process.env["APPDATA"] !== "undefined") {
appDataFolder = process.env["APPDATA"];
console.log(appDataFolder);
} else {
switch (process.platform) {
case "darwin":
appDataFolder = process.env["HOME"] + "/Library/Application Support";
console.log(appDataFolder);
break;
case "linux":
appDataFolder = process.env["HOME"] + "/.config";
console.log(appDataFolder);
break;
case "win32":
appDataFolder = "undefined";
break;
default:
appDataFolder = "undefined";
break;
}
}
var configFolder = path.join(appDataFolder, "FreeDATA");
var configPath = path.join(configFolder, "config.json");
console.log(appDataFolder);
console.log(configFolder);
console.log(configPath);
// create config folder if not exists
if (!fs.existsSync(configFolder)) {
fs.mkdirSync(configFolder);
}
// create config file if not exists with defaults
const configDefaultSettings =
'{\
"modem_host": "127.0.0.1",\
"modem_port": 3000,\
"daemon_host": "127.0.0.1",\
"daemon_port": 3001,\
"rx_audio" : "",\
"tx_audio" : "",\
"mycall": "AA0AA-0",\
"myssid": "0",\
"mygrid": "JN40aa",\
"radiocontrol" : "disabled",\
"hamlib_deviceid": 6,\
"hamlib_deviceport": "ignore",\
"hamlib_stop_bits": "ignore",\
"hamlib_data_bits": "ignore",\
"hamlib_handshake": "ignore",\
"hamlib_serialspeed": "ignore",\
"hamlib_dtrstate": "ignore",\
"hamlib_pttprotocol": "ignore",\
"hamlib_ptt_port": "ignore",\
"hamlib_dcd": "ignore",\
"hamlbib_serialspeed_ptt": 9600,\
"hamlib_rigctld_port" : 4532,\
"hamlib_rigctld_ip" : "127.0.0.1",\
"hamlib_rigctld_path" : "",\
"hamlib_rigctld_server_port" : 4532,\
"hamlib_rigctld_custom_args": "",\
"tci_port" : 50001,\
"tci_ip" : "127.0.0.1",\
"spectrum": "waterfall",\
"enable_scatter" : "False",\
"enable_fft" : "False",\
"enable_fsk" : "False",\
"low_bandwidth_mode" : "False",\
"theme" : "default",\
"screen_height" : 430,\
"screen_width" : 1050,\
"update_channel" : "latest",\
"beacon_interval" : 300,\
"received_files_folder" : "None",\
"tuning_range_fmin" : "-50.0",\
"tuning_range_fmax" : "50.0",\
"respond_to_cq" : "True",\
"rx_buffer_size" : 16, \
"enable_explorer" : "False", \
"wftheme": 2, \
"high_graphics" : "True",\
"explorer_stats" : "False", \
"auto_tune" : "False", \
"enable_is_writing" : "True", \
"shared_folder_path" : ".", \
"enable_request_profile" : "True", \
"enable_request_shared_folder" : "False", \
"max_retry_attempts" : 5, \
"enable_auto_retry" : "False", \
"tx_delay" : 0, \
"auto_start": 0, \
"enable_sys_notification": 1, \
"enable_mesh_features": "False" \
}';
if (!fs.existsSync(configPath)) {
fs.writeFileSync(configPath, configDefaultSettings);
}
export function loadSettings() {
// 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
console.log("CONFIG VALIDATION ----------------------------- ");
var parsedConfig = JSON.parse(configDefaultSettings);
for (var key in parsedConfig) {
if (config.hasOwnProperty(key)) {
console.log("FOUND SETTTING [" + key + "]: " + config[key]);
} else {
console.log("MISSING SETTTING [" + key + "] : " + parsedConfig[key]);
config[key] = parsedConfig[key];
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
}
try {
if (key == "mycall") {
settings.mycall = config[key].split("-")[0];
settings.myssid = config[key].split("-")[1];
} else {
settings[key] = config[key];
}
} catch (e) {
console.log(e);
}
}
}
export function saveSettingsToFile() {
console.log("save settings to file...");
let config = settings.getJSON();
console.log(config);
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
}

870
gui/src/js/sock.js Normal file
View file

@ -0,0 +1,870 @@
var net = require("net");
import { atob_FD, btoa_FD } from "./freedata";
import { addDataToWaterfall } from "../js/waterfallHandler.js";
import {
newMessageReceived,
newBeaconReceived,
updateTransmissionStatus,
setStateSuccess,
setStateFailed,
} from "./chatHandler";
import { displayToast } from "./popupHandler";
// ----------------- init pinia stores -------------
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useStateStore } from "../store/stateStore.js";
const stateStore = useStateStore(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
var client = new net.Socket();
var socketchunk = ""; // Current message, per connection.
// split character
//const split_char = "\0;\1;";
const split_char = "0;1;";
// global to keep track of Modem connection error emissions
var modemShowConnectStateError = 1;
// network connection Timeout
setTimeout(connectModem, 2000);
function connectModem() {
//exports.connectModem = function(){
//console.log('connecting to Modem...')
//clear message buffer after reconnecting or initial connection
socketchunk = "";
client.connect(settings.modem_port, settings.modem_host);
}
client.on("connect", function () {
console.log("Modem connection established");
stateStore.busy_state = "-";
stateStore.arq_state = "-";
stateStore.frequency = "-";
stateStore.mode = "-";
stateStore.bandwidth = "-";
stateStore.dbfs_level = 0;
stateStore.updateTncState(client.readyState);
modemShowConnectStateError = 1;
});
client.on("error", function (err) {
if (modemShowConnectStateError == 1) {
console.log("Modem connection error");
console.log(err);
modemShowConnectStateError = 0;
}
setTimeout(connectModem, 500);
client.destroy();
stateStore.busy_state = "-";
stateStore.arq_state = "-";
stateStore.frequency = "-";
stateStore.mode = "-";
stateStore.bandwidth = "-";
stateStore.dbfs_level = 0;
stateStore.updateTncState(client.readyState);
});
/*
client.on('close', function(data) {
console.log(' Modem connection closed');
setTimeout(connectModem, 2000)
});
*/
client.on("end", function (data) {
console.log("Modem connection ended");
console.log(data);
stateStore.busy_state = "-";
stateStore.arq_state = "-";
stateStore.frequency = "-";
stateStore.mode = "-";
stateStore.bandwidth = "-";
stateStore.dbfs_level = 0;
stateStore.updateTncState(client.readyState);
client.destroy();
setTimeout(connectModem, 500);
});
function writeTncCommand(command) {
console.log(command);
// we use the writingCommand function to update our TCPIP state because we are calling this function a lot
// if socket opened, we are able to run commands
if (client.readyState == "open") {
client.write(command + "\n");
}
if (client.readyState == "closed") {
console.log("Modem SOCKET CONNECTION CLOSED!");
}
if (client.readyState == "opening") {
console.log("connecting to Modem...");
}
}
client.on("data", function (socketdata) {
stateStore.updateTncState(client.readyState);
/*
inspired by:
stackoverflow.com questions 9070700 nodejs-net-createserver-large-amount-of-data-coming-in
*/
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");
//don't think this is needed anymore
//data = JSON.parse(socketchunk[0])
// search for empty entries in socketchunk and remove them
for (let i = 0; i < socketchunk.length; i++) {
if (socketchunk[i] === "") {
socketchunk.splice(i, 1);
}
}
//iterate through socketchunks array to execute multiple commands in row
for (let 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("Throwing away data!!!!\n" + e); // "SyntaxError
//console.log(e); // "SyntaxError
console.log(socketchunk[i]);
socketchunk = "";
//If we're here, I don't think we want to process any data that may be in data variable
continue;
}
}
//console.log(data)
if (data["command"] == "modem_state") {
//console.log(data)
stateStore.rx_buffer_length = data["rx_buffer_length"];
stateStore.frequency = data["frequency"];
stateStore.busy_state = data["modem_state"];
stateStore.arq_state = data["arq_state"];
stateStore.mode = data["mode"];
stateStore.bandwidth = data["bandwidth"];
stateStore.dbfs_level = data["audio_dbfs"];
stateStore.ptt_state = data["ptt_state"];
stateStore.speed_level = data["speed_level"];
stateStore.fft = JSON.parse(data["fft"]);
stateStore.channel_busy = data["channel_busy"];
stateStore.channel_busy_slot = data["channel_busy_slot"];
addDataToWaterfall(JSON.parse(data["fft"]));
if (data["scatter"].length > 0) {
stateStore.scatter = data["scatter"];
}
// s meter strength
stateStore.s_meter_strength_raw = data["strength"];
if (stateStore.s_meter_strength_raw == "") {
stateStore.s_meter_strength_raw = "Unsupported";
stateStore.s_meter_strength_percent = 0;
} else {
// https://www.moellerstudios.org/converting-amplitude-representations/
stateStore.s_meter_strength_percent = Math.round(
Math.pow(10, stateStore.s_meter_strength_raw / 20) * 100,
);
}
stateStore.dbfs_level_percent = Math.round(
Math.pow(10, stateStore.dbfs_level / 20) * 100,
);
stateStore.dbfs_level = Math.round(stateStore.dbfs_level);
stateStore.arq_total_bytes = data["total_bytes"];
stateStore.heard_stations = data["stations"];
stateStore.dxcallsign = data["dxcallsign"];
stateStore.beacon_state = data["beacon_state"];
stateStore.audio_recording = data["audio_recording"];
stateStore.hamlib_status = data["hamlib_status"];
stateStore.alc = data["alc"];
stateStore.rf_level = data["rf_level"];
stateStore.is_codec2_traffic = data["is_codec2_traffic"];
stateStore.arq_session_state = data["arq_session"];
stateStore.arq_state = data["arq_state"];
stateStore.arq_transmission_percent = data["arq_transmission_percent"];
stateStore.arq_seconds_until_finish = data["arq_seconds_until_finish"];
stateStore.arq_seconds_until_timeout =
data["arq_seconds_until_timeout"];
stateStore.arq_seconds_until_timeout_percent =
(stateStore.arq_seconds_until_timeout / 180) * 100;
if (data["speed_list"].length > 0) {
prepareStatsDataForStore(data["speed_list"]);
}
// TODO: Remove ported objects
/*
let Data = {
mycallsign: data["mycallsign"],
mygrid: data["mygrid"],
//channel_state: data['CHANNEL_STATE'],
info: data["info"],
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"],
routing_table: data["routing_table"],
mesh_signalling_table: data["mesh_signalling_table"],
listen: data["listen"],
//speed_table: [{"bpm" : 5200, "snr": -3, "timestamp":1673555399},{"bpm" : 2315, "snr": 12, "timestamp":1673555500}],
};
*/
//continue to next for loop iteration, nothing else needs to be done here
continue;
}
// ----------- catch modem messages START -----------
//init message variable
var message = "";
if (data["freedata"] == "modem-message") {
// break early if we received a dummy callsign
// thats a kind of hotfix, as long as the modem isnt handling this better
if (
data["dxcallsign"] == "AA0AA-0" ||
data["dxcallsign"] == "ZZ9YY-0"
) {
break;
}
console.log(data);
switch (data["fec"]) {
case "is_writing":
// RX'd FECiswriting
break;
case "broadcast":
// RX'd FEC BROADCAST
var encoded_data = atob_FD(data["data"]);
var splitted_data = encoded_data.split(split_char);
var messageArray = [];
if (splitted_data[0] == "m") {
messageArray.push(data);
console.log(data);
}
break;
}
switch (data["cq"]) {
case "transmitting":
// CQ TRANSMITTING
displayToast(
"success",
"bi-arrow-left-right",
"Transmitting CQ",
5000,
);
break;
case "received":
// CQ RECEIVED
message = "CQ from " + data["dxcallsign"];
displayToast("success", "bi-person-arms-up", message, 5000);
break;
}
switch (data["qrv"]) {
case "transmitting":
// QRV TRANSMITTING
displayToast(
"info",
"bi-person-raised-hand",
"Transmitting QRV ",
5000,
);
break;
case "received":
// QRV RECEIVED
message = "QRV from " + data["dxcallsign"] + " | " + data["dxgrid"];
displayToast("success", "bi-person-raised-hand", message, 5000);
break;
}
switch (data["beacon"]) {
case "transmitting":
// BEACON TRANSMITTING
displayToast(
"success",
"bi-broadcast-pin",
"Transmitting beacon",
5000,
);
break;
case "received":
// BEACON RECEIVED
newBeaconReceived(data);
message =
"Beacon from " + data["dxcallsign"] + " | " + data["dxgrid"];
displayToast("info", "bi-broadcast", message, 5000);
break;
}
switch (data["ping"]) {
case "transmitting":
// PING TRANSMITTING
message = "Sending ping to " + data["dxcallsign"];
displayToast("success", "bi-arrow-right", message, 5000);
break;
case "received":
// PING RECEIVED
message =
"Ping request from " +
data["dxcallsign"] +
" | " +
data["dxgrid"];
displayToast("success", "bi-arrow-right-short", message, 5000);
break;
case "acknowledge":
// PING ACKNOWLEDGE
message =
"Received ping-ack from " +
data["dxcallsign"] +
" | " +
data["dxgrid"];
displayToast("success", "bi-arrow-left-right", message, 5000);
break;
}
// ARQ SESSION && freedata == modem-message
if (data["arq"] == "session") {
switch (data["status"]) {
case "connecting":
// ARQ Open
break;
case "connected":
// ARQ Opening
break;
case "waiting":
// ARQ Opening
break;
case "close":
// ARQ Closing
break;
case "failed":
// ARQ Failed
break;
}
}
// ARQ TRANSMISSION && freedata == modem-message
if (data["arq"] == "transmission") {
switch (data["status"]) {
case "opened":
// ARQ Open
message = "ARQ session opened: " + data["dxcallsign"];
displayToast("success", "bi-arrow-left-right", message, 5000);
break;
case "opening":
// ARQ Opening IRS/ISS
if (data["irs"] == "False") {
message = "ARQ session opening: " + data["dxcallsign"];
displayToast("info", "bi-arrow-left-right", message, 5000);
break;
} else {
message = "ARQ sesson request from: " + data["dxcallsign"];
displayToast("success", "bi-arrow-left-right", message, 5000);
break;
}
case "waiting":
// ARQ waiting
message = "Channel busy | ARQ protocol is waiting";
displayToast("warning", "bi-hourglass-split", message, 5000);
break;
case "receiving":
// ARQ RX
break;
case "failed":
// ARQ TX Failed
if (data["reason"] == "protocol version missmatch") {
message = "Protocol version mismatch!";
displayToast("danger", "bi-chevron-bar-expand", message, 5000);
setStateFailed();
break;
} else {
message = "Transmission failed";
displayToast("danger", "bi-x-octagon", message, 5000);
updateTransmissionStatus(data);
setStateFailed();
break;
}
switch (data["irs"]) {
case "True":
updateTransmissionStatus(data);
break;
default:
updateTransmissionStatus(data);
break;
}
break;
case "received":
// ARQ data received
console.log(data);
// we need to encode here to do a deep check for checking if file or message
//var encoded_data = atob(data['data'])
var encoded_data = atob_FD(data["data"]);
var splitted_data = encoded_data.split(split_char);
// new message received
if (splitted_data[0] == "m") {
console.log(splitted_data);
newMessageReceived(splitted_data, data);
}
break;
case "transmitting":
// ARQ transmitting
updateTransmissionStatus(data);
break;
case "transmitted":
// ARQ transmitted
message = "Data transmitted";
displayToast("success", "bi-check-sqaure", message, 5000);
updateTransmissionStatus(data);
setStateSuccess();
break;
}
}
}
}
//finally delete message buffer
socketchunk = "";
}
});
// Send Ping
//exports.sendPing = function (dxcallsign) {
export function sendPing(dxcallsign) {
var command =
'{"type" : "ping", "command" : "ping", "dxcallsign" : "' +
dxcallsign +
'"}';
writeTncCommand(command);
}
// Send Mesh Ping
//exports.sendMeshPing = function (dxcallsign) {
function sendMeshPing(dxcallsign) {
var command =
'{"type" : "mesh", "command" : "ping", "dxcallsign" : "' +
dxcallsign +
'"}';
writeTncCommand(command);
}
// Send CQ
//exports.sendCQ = function () {
export function sendCQ() {
var command = '{"type" : "broadcast", "command" : "cqcqcq"}';
writeTncCommand(command);
}
// Set AUDIO Level
export function setTxAudioLevel(value) {
var command =
'{"type" : "set", "command" : "tx_audio_level", "value" : "' + value + '"}';
writeTncCommand(command);
}
// Send Message
export function sendMessage(obj) {
let dxcallsign = obj.dxcallsign;
let checksum = obj.checksum;
let uuid = obj.uuid;
let command = obj.command;
let filename = Object.keys(obj._attachments)[0];
//let filetype = filename.split(".")[1]
let filetype = obj._attachments[filename].content_type;
let file = obj._attachments[filename].data;
//console.log(obj._attachments)
//console.log(filename)
//console.log(filetype)
//console.log(file)
let data_with_attachment =
obj.timestamp +
split_char +
obj.msg +
split_char +
filename +
split_char +
filetype +
split_char +
file;
let data = btoa_FD(
"m" +
split_char +
command +
split_char +
checksum +
split_char +
uuid +
split_char +
data_with_attachment,
);
// TODO: REMOVE mode and frames from Modem!
var mode = 255;
var frames = 5;
command =
'{"type" : "arq", "command" : "send_raw", "uuid" : "' +
uuid +
'", "parameter" : [{"dxcallsign" : "' +
dxcallsign +
'", "mode" : "' +
mode +
'", "n_frames" : "' +
frames +
'", "data" : "' +
data +
'", "attempts": "10"}]}';
console.log(command);
writeTncCommand(command);
}
/*
// Send Request message
//It would be then „m + split + request + split + request-type“
function sendRequest(dxcallsign, mode, frames, data, command) {
data = btoa_FD("m" + split_char + command + split_char + data);
command =
'{"type" : "arq", "command" : "send_raw", "parameter" : [{"dxcallsign" : "' +
dxcallsign +
'", "mode" : "' +
mode +
'", "n_frames" : "' +
frames +
'", "data" : "' +
data +
'", "attempts": "10"}]}';
console.log(command);
console.log("--------------REQ--------------------");
writeTncCommand(command);
}
// Send Response message
//It would be then „m + split + request + split + request-type“
function sendResponse(dxcallsign, mode, frames, data, command) {
data = btoa_FD("m" + split_char + command + split_char + data);
command =
'{"type" : "arq", "command" : "send_raw", "parameter" : [{"dxcallsign" : "' +
dxcallsign +
'", "mode" : "' +
mode +
'", "n_frames" : "' +
frames +
'", "data" : "' +
data +
'", "attempts": "10"}]}';
console.log(command);
console.log("--------------RES--------------------");
writeTncCommand(command);
}
//Send station info request
//exports.sendRequestInfo = function (dxcallsign) {
function sendRequestInfo(dxcallsign) {
//Command 0 = user/station information
//Command 1 = shared folder list
//Command 2 = shared file transfer
sendRequest(dxcallsign, 255, 1, "0", "req");
}
//Send shared folder file list request
//exports.sendRequestSharedFolderList = function (dxcallsign) {
function sendRequestSharedFolderList(dxcallsign) {
//Command 0 = user/station information
//Command 1 = shared folder list
//Command 2 = shared file transfer
sendRequest(dxcallsign, 255, 1, "1", "req");
}
//Send shared file request
//exports.sendRequestSharedFile = function (dxcallsign, file) {
function sendRequestSharedFile(dxcallsign, file) {
//Command 0 = user/station information
//Command 1 = shared folder list
//Command 2 = shared file transfer
sendRequest(dxcallsign, 255, 1, "2" + file, "req");
}
//Send station info response
//exports.sendResponseInfo = function (dxcallsign, userinfo) {
function sendResponseInfo(dxcallsign, userinfo) {
//Command 0 = user/station information
//Command 1 = shared folder list
//Command 2 = shared file transfer
sendResponse(dxcallsign, 255, 1, userinfo, "res-0");
}
//Send shared folder response
//exports.sendResponseSharedFolderList = function (dxcallsign, sharedFolderList) {
function sendResponseSharedFolderList(dxcallsign, sharedFolderList) {
//Command 0 = user/station information
//Command 1 = shared folder list
//Command 2 = shared file transfer
sendResponse(dxcallsign, 255, 1, sharedFolderList, "res-1");
}
//Send shared file response
//exports.sendResponseSharedFile = function (
function sendResponseSharedFile(dxcallsign, sharedFile, sharedFileData) {
console.log(
"In sendResponseSharedFile",
dxcallsign,
sharedFile,
sharedFileData,
);
//Command 0 = user/station information
//Command 1 = shared folder list
//Command 2 = shared file transfer
sendResponse(dxcallsign, 255, 1, sharedFile + "/" + sharedFileData, "res-2");
}
*/
//STOP TRANSMISSION
export function stopTransmission() {
var command = '{"type" : "arq", "command": "stop_transmission"}';
writeTncCommand(command);
}
// Get RX BUffer
export function getRxBuffer() {
var command = '{"type" : "get", "command" : "rx_buffer"}';
writeTncCommand(command);
}
// START BEACON
export function startBeacon(interval) {
var command =
'{"type" : "broadcast", "command" : "start_beacon", "parameter": "' +
interval +
'"}';
writeTncCommand(command);
}
// STOP BEACON
export function stopBeacon() {
var command = '{"type" : "broadcast", "command" : "stop_beacon"}';
writeTncCommand(command);
}
// OPEN ARQ SESSION
export function connectARQ(dxcallsign) {
var command =
'{"type" : "arq", "command" : "connect", "dxcallsign": "' +
dxcallsign +
'", "attempts": "10"}';
writeTncCommand(command);
}
// CLOSE ARQ SESSION
export function disconnectARQ() {
var command = '{"type" : "arq", "command" : "disconnect"}';
writeTncCommand(command);
}
// SEND TEST FRAME
export function sendTestFrame() {
var command = '{"type" : "set", "command" : "send_test_frame"}';
writeTncCommand(command);
}
// SEND FEC
export function sendFEC(mode, payload) {
var command =
'{"type" : "fec", "command" : "transmit", "mode" : "' +
mode +
'", "payload" : "' +
payload +
'"}';
writeTncCommand(command);
}
// SEND FEC IS WRITING
export function sendFecIsWriting(mycallsign) {
var command =
'{"type" : "fec", "command" : "transmit_is_writing", "mycallsign" : "' +
mycallsign +
'"}';
writeTncCommand(command);
}
// SEND FEC TO BROADCASTCHANNEL
//export function sendBroadcastChannel(channel, data_out, uuid) {
export function sendBroadcastChannel(obj) {
//let checksum = obj.checksum;
let command = obj.command;
let uuid = obj.uuid;
let channel = obj.dxcallsign;
let data_out = obj.msg;
let data = btoa_FD(
"m" +
split_char +
channel +
//split_char +
//checksum +
split_char +
uuid +
split_char +
data_out,
);
console.log(data.length);
let payload = data;
command =
'{"type" : "fec", "command" : "transmit", "mode": "datac4", "wakeup": "True", "payload" : "' +
payload +
'"}';
writeTncCommand(command);
}
// RECORD AUDIO
export function record_audio() {
var command = '{"type" : "set", "command" : "record_audio"}';
writeTncCommand(command);
}
// SET FREQUENCY
export function set_frequency(frequency) {
var command =
'{"type" : "set", "command" : "frequency", "frequency": ' + frequency + "}";
writeTncCommand(command);
}
// SET MODE
export function set_mode(mode) {
var command = '{"type" : "set", "command" : "mode", "mode": "' + mode + '"}';
writeTncCommand(command);
}
// SET rf_level
export function set_rf_level(rf_level) {
var command =
'{"type" : "set", "command" : "rf_level", "rf_level": "' + rf_level + '"}';
writeTncCommand(command);
}
// https://stackoverflow.com/a/50579690
// crc32 calculation
//console.log(crc32('abc'));
//console.log(crc32('abc').toString(16).toUpperCase()); // hex
/*
function crc32(r) {
for (var a, o = [], c = 0; c < 256; c++) {
a = c;
for (var f = 0; f < 8; f++) a = 1 & a ? 3988292384 ^ (a >>> 1) : a >>> 1;
o[c] = a;
}
for (var n = -1, t = 0; t < r.length; t++)
n = (n >>> 8) ^ o[255 & (n ^ r.charCodeAt(t))];
return (-1 ^ n) >>> 0;
};
*/
// TODO Maybe moving this to another module
function prepareStatsDataForStore(data) {
// dummy data
//state.arq_speed_list = [{"snr":0.0,"bpm":104,"timestamp":1696189769},{"snr":0.0,"bpm":80,"timestamp":1696189778},{"snr":0.0,"bpm":70,"timestamp":1696189783},{"snr":0.0,"bpm":58,"timestamp":1696189792},{"snr":0.0,"bpm":52,"timestamp":1696189797},{"snr":"NaN","bpm":42,"timestamp":1696189811},{"snr":0.0,"bpm":22,"timestamp":1696189875},{"snr":0.0,"bpm":21,"timestamp":1696189881},{"snr":0.0,"bpm":17,"timestamp":1696189913},{"snr":0.0,"bpm":15,"timestamp":1696189932},{"snr":0.0,"bpm":15,"timestamp":1696189937},{"snr":0.0,"bpm":14,"timestamp":1696189946},{"snr":-6.1,"bpm":14,"timestamp":1696189954},{"snr":-6.1,"bpm":14,"timestamp":1696189955},{"snr":-5.5,"bpm":28,"timestamp":1696189963},{"snr":-5.5,"bpm":27,"timestamp":1696189963}]
var speed_listSize = 0;
if (typeof data == "undefined") {
speed_listSize = 0;
} else {
speed_listSize = data.length;
}
var speed_list_bpm = [];
for (let i = 0; i < speed_listSize; i++) {
speed_list_bpm.push(data[i].bpm);
}
var speed_list_timestamp = [];
for (let i = 0; i < speed_listSize; i++) {
let timestamp = data[i].timestamp * 1000;
let h = new Date(timestamp).getHours();
let m = new Date(timestamp).getMinutes();
let s = new Date(timestamp).getSeconds();
let time = h + ":" + m + ":" + s;
speed_list_timestamp.push(time);
}
var speed_list_snr = [];
for (let i = 0; i < speed_listSize; i++) {
let snr = NaN;
if (data[i].snr !== 0) {
snr = data[i].snr;
} else {
snr = NaN;
}
speed_list_snr.push(snr);
}
stateStore.arq_speed_list_bpm = speed_list_bpm;
stateStore.arq_speed_list_timestamp = speed_list_timestamp;
stateStore.arq_speed_list_snr = speed_list_snr;
}

View file

@ -0,0 +1,29 @@
import { Spectrum } from "../assets/waterfall/spectrum.js";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
var spectrum = new Object();
export function initWaterfall() {
spectrum = new Spectrum("waterfall", {
spectrumPercent: 0,
wf_rows: 192, //Assuming 1 row = 1 pixe1, 192 is the height of the spectrum container
});
console.log(settings.wftheme);
spectrum.setColorMap(settings.wftheme);
}
export function addDataToWaterfall(data) {
//console.log(spectrum)
try {
spectrum.addData(data);
} catch (e) {
//console.log(e);
}
}

View file

@ -1,92 +0,0 @@
<!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"
/>
<link rel="stylesheet" type="text/css" href="styles.css" />
<title>FreeDATA - Live Log</title>
</head>
<body>
<!-- bootstrap -->
<script src="../node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<!-- chart.js -->
<nav class="navbar fixed-top bg-light">
<div class="container-fluid">
<input
type="checkbox"
class="btn-check"
id="enable_filter_info"
autocomplete="off"
checked
/>
<label class="btn btn-outline-info" for="enable_filter_info"
>info</label
>
<input
type="checkbox"
class="btn-check"
id="enable_filter_debug"
autocomplete="off"
/>
<label class="btn btn-outline-primary" for="enable_filter_debug"
>debug</label
>
<input
type="checkbox"
class="btn-check"
id="enable_filter_warning"
autocomplete="off"
/>
<label class="btn btn-outline-warning" for="enable_filter_warning"
>warning</label
>
<input
type="checkbox"
class="btn-check"
id="enable_filter_error"
autocomplete="off"
/>
<label class="btn btn-outline-danger" for="enable_filter_error"
>error</label
>
</div>
</nav>
<div class="container-fluid mt-5">
<div class="tableFixHead">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Timestamp</th>
<th scope="col">Type</th>
<th scope="col">Area</th>
<th scope="col">Log entry</th>
</tr>
</thead>
<tbody id="log">
<!--
<tr>
<th scope="row">1</th>
<td>Mark</td>
<td>Otto</td>
<td>@mdo</td>
</tr>
-->
</tbody>
</table>
</div>
</div>
</body>
</html>

29
gui/src/main.ts Normal file
View file

@ -0,0 +1,29 @@
import { createApp } from "vue";
import { createPinia } from "pinia";
import { loadSettings } from "./js/settingsHandler";
import "./styles.css";
// Import all of Bootstrap's JS
//import * as bootstrap from 'bootstrap'
import "bootstrap/dist/js/bootstrap.bundle.min.js";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-icons/font/bootstrap-icons.css";
// Import our custom CSS
//import './scss/styles.scss'
import App from "./App.vue";
const app = createApp(App);
//.mount('#app').$nextTick(() => postMessage({ payload: 'removeLoading' }, '*'))
const pinia = createPinia();
app.mount("#app");
app.use(pinia);
loadSettings();
//import './js/settingsHandler.js'
import "./js/daemon";
import "./js/sock.js";
//import './js/settingsHandler.js'

View file

@ -1,147 +0,0 @@
<!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"
/>
<link
rel="stylesheet"
href="../node_modules/bootstrap-icons/font/bootstrap-icons.css"
/>
<link rel="stylesheet" type="text/css" href="styles.css" />
<title>FreeDATA - Mesh Table</title>
</head>
<body>
<!-- bootstrap -->
<script src="../node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<!-- chart.js -->
<nav>
<div class="nav nav-tabs" id="nav-tab" role="tablist">
<button
class="nav-link active"
id="nav-route-tab"
data-bs-toggle="tab"
data-bs-target="#nav-route"
type="button"
role="tab"
aria-controls="nav-route"
aria-selected="true"
>
Routes
</button>
<button
class="nav-link"
id="nav-signaling-tab"
data-bs-toggle="tab"
data-bs-target="#nav-signaling"
type="button"
role="tab"
aria-controls="nav-signaling"
aria-selected="false"
>
Signaling
</button>
<button
class="nav-link"
id="nav-actions-tab"
data-bs-toggle="tab"
data-bs-target="#nav-actions"
type="button"
role="tab"
aria-controls="nav-actions"
aria-selected="false"
>
Actions
</button>
</div>
</nav>
<div class="tab-content" id="nav-tabContent">
<div
class="tab-pane fade show active vw-100 vh-90 overflow-auto"
id="nav-route"
role="tabpanel"
aria-labelledby="nav-route-tab"
>
<div class="container-fluid">
<div
class="table-responsive overflow-auto"
style="max-width: 99vw; max-height: 99vh"
>
<table class="table table-hover table-sm">
<thead>
<tr>
<th scope="col">Timestamp</th>
<th scope="col">DXCall</th>
<th scope="col">Router</th>
<th scope="col">Hops</th>
<th scope="col">Score</th>
<th scope="col">SNR</th>
</tr>
</thead>
<tbody id="mesh-table"></tbody>
</table>
</div>
</div>
</div>
<div
class="tab-pane fade"
id="nav-signaling"
role="tabpanel"
aria-labelledby="nav-signaling-tab"
>
<div class="container-fluid">
<div
class="table-responsive overflow-auto"
style="max-width: 99vw; max-height: 99vh"
>
<table class="table table-hover table-sm">
<thead>
<tr>
<th scope="col">Timestamp</th>
<th scope="col">Destination</th>
<th scope="col">Origin</th>
<th scope="col">Frametype</th>
<th scope="col">Payload</th>
<th scope="col">Attempt</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody id="mesh-signalling-table"></tbody>
</table>
</div>
</div>
</div>
<div
class="tab-pane fade"
id="nav-actions"
role="tabpanel"
aria-labelledby="nav-actions-tab"
>
<div class="input-group mt-1">
<input
type="text"
class="form-control"
style="max-width: 6rem; text-transform: uppercase"
placeholder="DXcall"
pattern="[A-Z]*"
id="dxCallMesh"
maxlength="11"
aria-label="Input group"
aria-describedby="btnGroupAddon"
/>
<button id="transmit_mesh_ping" type="button" class="btn btn-primary">
mesh ping
</button>
</div>
</div>
</div>
</body>
</html>

2
gui/src/scss/styles.scss Normal file
View file

@ -0,0 +1,2 @@
// Import all of Bootstrap's CSS
@import "bootstrap/scss/bootstrap";

View file

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

View file

@ -0,0 +1,55 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
export const useAudioStore = defineStore("audioStore", () => {
var inputDevices = ref([{ id: 0, name: "no input devices" }]);
var outputDevices = ref([{ id: 0, name: "no output devices" }]);
var startupInputDevice = ref(0);
var startupOutputDevice = ref(0);
function getInputDevices() {
var html = "";
for (var key in inputDevices.value) {
let selected = "";
if (inputDevices.value[key]["name"] == settings.rx_audio) {
selected = "selected";
} else {
selected = "";
}
html += `<option value=${inputDevices.value[key]["id"]} ${selected}>${inputDevices.value[key]["name"]}</option>`;
}
return html;
}
function getOutputDevices() {
var html = "";
for (var key in outputDevices.value) {
let selected = "";
if (outputDevices.value[key]["name"] == settings.tx_audio) {
selected = "selected";
} else {
selected = "";
}
html += `<option value=${outputDevices.value[key]["id"]} ${selected}>${outputDevices.value[key]["name"]}</option>`;
}
return html;
}
return {
inputDevices,
outputDevices,
getInputDevices,
getOutputDevices,
startupInputDevice,
startupOutputDevice,
};
});

View file

@ -0,0 +1,92 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useChatStore = defineStore("chatStore", () => {
var chat_filter = ref([
{ type: "newchat" },
{ type: "received" },
{ type: "transmit" },
{ type: "ping-ack" },
{ type: "broadcast_received" },
{ type: "broadcast_transmit" },
//{ type: "request" },
//{ type: "response" },
]);
var selectedCallsign = ref();
// we need a default value in our ref because of our message info modal
var selectedMessageObject = ref({
command: "msg",
hmac_signed: false,
percent: 0,
is_new: false,
_id: "2ead6698",
timestamp: 1697289795,
dxcallsign: "DJ2LS-0",
dxgrid: "null",
msg: "test",
checksum: "",
type: "transmit",
status: "transmitting",
attempt: 1,
uuid: "2ead6698",
duration: 0,
nacks: 0,
speed_list: "null",
_attachments: {
"": {
content_type: "text",
data: "",
},
},
});
var inputText = ref("");
var inputFile = ref();
var inputFileName = ref();
var inputFileType = ref();
var inputFileSize = ref();
var callsign_list = ref();
var sorted_chat_list = ref();
var unsorted_chat_list = ref([]);
var sorted_beacon_list = ref({});
var unsorted_beacon_list = ref({});
var chartSpeedPER0 = ref();
var chartSpeedPER25 = ref();
var chartSpeedPER75 = ref();
// var beaconDataArray = ref([-3, 10, 8, 5, 3, 0, -5, 10, 8, 5, 3, 0, -5, 10, 8, 5, 3, 0, -5, 10, 8, 5, 3, 0, -5])
// var beaconLabelArray = ref(['18:10', '19:00', '23:00', '01:13', '04:25', '08:15', '09:12', '18:10', '19:00', '23:00', '01:13', '04:25', '08:15', '09:12', '18:10', '19:00', '23:00', '01:13', '04:25', '08:15', '09:12', '01:13', '04:25', '08:15', '09:12'])
var beaconDataArray = ref([]);
var beaconLabelArray = ref([]);
var arq_speed_list_bpm = ref([]);
var arq_speed_list_timestamp = ref([]);
var arq_speed_list_snr = ref([]);
return {
selectedCallsign,
selectedMessageObject,
inputText,
chat_filter,
callsign_list,
sorted_chat_list,
unsorted_chat_list,
inputFileName,
inputFileSize,
inputFileType,
inputFile,
chartSpeedPER0,
chartSpeedPER25,
chartSpeedPER75,
beaconDataArray,
beaconLabelArray,
unsorted_beacon_list,
sorted_beacon_list,
arq_speed_list_bpm,
arq_speed_list_snr,
arq_speed_list_timestamp,
};
});

5
gui/src/store/index.js Normal file
View file

@ -0,0 +1,5 @@
import { createPinia } from "pinia";
const pinia = createPinia();
export default pinia;

View file

@ -0,0 +1,228 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useSettingsStore = defineStore("settingsStore", () => {
// audio
var tx_audio = ref();
var rx_audio = ref();
// network
var modem_host = ref("127.0.0.1");
var modem_port = ref(3000);
var daemon_host = ref(modem_host.value);
var daemon_port = ref(modem_port.value + 1);
// app
var screen_height = ref(430);
var screen_width = ref(1050);
var theme = ref("default");
var wftheme = ref(2);
var high_graphics = ref("False");
var auto_start = ref(0);
var enable_sys_notification = ref(1);
// chat
var shared_folder_path = ref(".");
var enable_request_profile = ref("True");
var enable_request_shared_folder = ref("False");
var max_retry_attempts = ref(5);
var enable_auto_retry = ref("False");
// station
var mycall = ref("AA0AA-5");
var myssid = ref(0);
var mygrid = ref("JN20aa");
// rigctld
var hamlib_rigctld_port = ref(4532);
var hamlib_rigctld_ip = ref("127.0.0.1");
var radiocontrol = ref("disabled");
var hamlib_deviceid = ref("RIG_MODEL_DUMMY_NOVFO");
var hamlib_deviceport = ref("ignore");
var hamlib_stop_bits = ref("ignore");
var hamlib_data_bits = ref("ignore");
var hamlib_handshake = ref("ignore");
var hamlib_serialspeed = ref("ignore");
var hamlib_dtrstate = ref("ignore");
var hamlib_pttprotocol = ref("ignore");
var hamlib_ptt_port = ref("ignore");
var hamlib_dcd = ref("ignore");
var hamlbib_serialspeed_ptt = ref(9600);
var hamlib_rigctld_path = ref("");
var hamlib_rigctld_server_port = ref(4532);
var hamlib_rigctld_custom_args = ref("");
// tci
var tci_ip = ref("127.0.0.1");
var tci_port = ref(50001);
//modem
var spectrum = ref("waterfall");
var enable_scatter = ref("False");
var enable_fft = ref("False");
var enable_fsk = ref("False");
var low_bandwidth_mode = ref("False");
var update_channel = ref("latest");
var beacon_interval = ref(300);
var received_files_folder = ref("None");
var tuning_range_fmin = ref(-50.0);
var tuning_range_fmax = ref(50.0);
var respond_to_cq = ref("True");
var rx_buffer_size = ref(16);
var enable_explorer = ref("False");
var explorer_stats = ref("False");
var auto_tune = ref("False");
var enable_is_writing = ref("True");
var tx_delay = ref(0);
var enable_mesh_features = ref("False");
var serial_devices = ref();
function getSerialDevices() {
if (this.hamlib_deviceport == "ignore")
var html =
'<option value ="ignore" selected>None - (use custom options for hamlib)</option>';
else
var html =
'<option value ="ignore">None - (use custom options for hamlib)</option>';
for (var key in serial_devices.value) {
let selected = "";
if (serial_devices.value[key]["port"] == this.hamlib_deviceport) {
selected = "selected";
} else {
selected = "";
}
html += `<option value="${serial_devices.value[key]["port"]}" ${selected}>${serial_devices.value[key]["port"]} - ${serial_devices.value[key]["description"]}</option>`;
}
return html;
}
function getJSON() {
var config_export = {
modem_host: modem_host.value,
modem_port: modem_port.value,
daemon_host: modem_host.value,
daemon_port: (parseInt(modem_port.value) + 1).toString(),
mycall: mycall.value,
myssid: myssid.value,
mygrid: mygrid.value,
radiocontrol: radiocontrol.value,
hamlib_deviceid: hamlib_deviceid.value,
hamlib_deviceport: hamlib_deviceport.value,
hamlib_stop_bits: hamlib_stop_bits.value,
hamlib_data_bits: hamlib_data_bits.value,
hamlib_handshake: hamlib_handshake.value,
hamlib_serialspeed: hamlib_serialspeed.value,
hamlib_dtrstate: hamlib_dtrstate.value,
hamlib_pttprotocol: hamlib_pttprotocol.value,
hamlib_ptt_port: hamlib_ptt_port.value,
hamlib_dcd: hamlib_dcd.value,
hamlbib_serialspeed_ptt: hamlib_serialspeed.value,
hamlib_rigctld_port: hamlib_rigctld_port.value,
hamlib_rigctld_ip: hamlib_rigctld_ip.value,
hamlib_rigctld_path: hamlib_rigctld_path.value,
hamlib_rigctld_server_port: hamlib_rigctld_server_port.value,
hamlib_rigctld_custom_args: hamlib_rigctld_custom_args.value,
tci_port: tci_port.value,
tci_ip: tci_ip.value,
spectrum: spectrum.value,
enable_scatter: enable_scatter.value,
enable_fft: enable_fft.value,
enable_fsk: enable_fsk.value,
low_bandwidth_mode: low_bandwidth_mode.value,
theme: theme.value,
screen_height: screen_height.value,
screen_width: screen_width.value,
update_channel: update_channel.value,
beacon_interval: beacon_interval.value,
received_files_folder: received_files_folder.value,
tuning_range_fmin: tuning_range_fmin.value,
tuning_range_fmax: tuning_range_fmax.value,
respond_to_cq: respond_to_cq.value,
rx_buffer_size: rx_buffer_size.value,
enable_explorer: enable_explorer.value,
wftheme: wftheme.value,
high_graphics: high_graphics.value,
explorer_stats: explorer_stats.value,
auto_tune: auto_tune.value,
enable_is_writing: enable_is_writing.value,
shared_folder_path: shared_folder_path.value,
enable_request_profile: enable_request_profile.value,
enable_request_shared_folder: enable_request_shared_folder.value,
max_retry_attempts: max_retry_attempts.value,
enable_auto_retry: enable_auto_retry.value,
tx_delay: tx_delay.value,
auto_start: auto_start.value,
enable_sys_notification: enable_sys_notification.value,
enable_mesh_features: enable_mesh_features.value,
tx_audio: tx_audio.value,
rx_audio: rx_audio.value,
};
return config_export;
}
return {
modem_host,
modem_port,
daemon_host,
daemon_port,
screen_height,
screen_width,
theme,
wftheme,
high_graphics,
auto_start,
enable_sys_notification,
shared_folder_path,
enable_request_profile,
enable_request_shared_folder,
max_retry_attempts,
enable_auto_retry,
mycall,
myssid,
mygrid,
hamlib_rigctld_port,
hamlib_rigctld_ip,
radiocontrol,
hamlib_deviceid,
hamlib_deviceport,
hamlib_stop_bits,
hamlib_data_bits,
hamlib_handshake,
hamlib_serialspeed,
hamlib_dtrstate,
hamlib_pttprotocol,
hamlib_ptt_port,
hamlib_dcd,
hamlbib_serialspeed_ptt,
hamlib_rigctld_path,
hamlib_rigctld_server_port,
hamlib_rigctld_custom_args,
tci_ip,
tci_port,
spectrum,
enable_scatter,
enable_fft,
enable_fsk,
low_bandwidth_mode,
update_channel,
beacon_interval,
received_files_folder,
tuning_range_fmin,
tuning_range_fmax,
respond_to_cq,
rx_buffer_size,
enable_explorer,
explorer_stats,
auto_tune,
enable_is_writing,
tx_delay,
enable_mesh_features,
getJSON,
tx_audio,
rx_audio,
getSerialDevices,
serial_devices,
};
});

183
gui/src/store/stateStore.js Normal file
View file

@ -0,0 +1,183 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import * as bootstrap from "bootstrap";
export const useStateStore = defineStore("stateStore", () => {
var busy_state = ref("-");
var arq_state = ref("-");
var frequency = ref("-");
var new_frequency = ref(0);
var mode = ref("-");
var rf_level = ref("10");
var bandwidth = ref("-");
var dbfs_level_percent = ref(0);
var dbfs_level = ref(0);
var ptt_state = ref("False");
var speed_level = ref(0);
var fft = ref();
var channel_busy = ref("");
var channel_busy_slot = ref();
var scatter = ref();
var s_meter_strength_percent = ref(0);
var s_meter_strength_raw = ref(0);
var modem_connection = ref("disconnected");
var modemStartCount = ref(0);
var modem_running_state = ref("--------");
var arq_total_bytes = ref(0);
var arq_transmission_percent = ref(0);
var heard_stations = ref("");
var dxcallsign = ref("");
var arq_session_state = ref("");
var arq_state = ref("");
var beacon_state = ref("False");
var audio_recording = ref("");
var hamlib_status = ref("");
var audio_level = ref("");
var alc = ref("");
var is_codec2_traffic = ref("");
var arq_speed_list_timestamp = ref([]);
var arq_speed_list_bpm = ref([]);
var arq_speed_list_snr = ref([]);
var arq_seconds_until_finish = ref();
var arq_seconds_until_timeout = ref();
var arq_seconds_until_timeout_percent = ref();
var rigctld_started = ref();
var rigctld_process = ref();
var python_version = ref();
var modem_version = ref();
var rx_buffer_length = ref();
function getChannelBusySlotState(slot) {
const slot_state = channel_busy_slot.value;
if (typeof slot_state !== "undefined") {
// Replace 'False' with 'false' to match JavaScript's boolean representation
const string = slot_state
.replace(/False/g, "false")
.replace(/True/g, "true");
// Parse the string to get an array
const arr = JSON.parse(string);
return arr[slot];
} else {
// Handle the undefined case
return false;
}
}
function updateTncState(state) {
modem_connection.value = state;
if (modem_connection.value == "open") {
// collapse settings screen
var collapseFirstRow = new bootstrap.Collapse(
document.getElementById("collapseFirstRow"),
{ toggle: false },
);
collapseFirstRow.hide();
var collapseSecondRow = new bootstrap.Collapse(
document.getElementById("collapseSecondRow"),
{ toggle: false },
);
collapseSecondRow.hide();
var collapseThirdRow = new bootstrap.Collapse(
document.getElementById("collapseThirdRow"),
{ toggle: false },
);
collapseThirdRow.show();
var collapseFourthRow = new bootstrap.Collapse(
document.getElementById("collapseFourthRow"),
{ toggle: false },
);
collapseFourthRow.show();
//Set tuning for fancy graphics mode (high/low CPU)
//set_CPU_mode();
//GUI will auto connect to TNC if already running, if that is the case increment start count if 0
if (modemStartCount.value == 0) modemStartCount.value++;
} else {
// collapse settings screen
var collapseFirstRow = new bootstrap.Collapse(
document.getElementById("collapseFirstRow"),
{ toggle: false },
);
collapseFirstRow.show();
var collapseSecondRow = new bootstrap.Collapse(
document.getElementById("collapseSecondRow"),
{ toggle: false },
);
collapseSecondRow.show();
var collapseThirdRow = new bootstrap.Collapse(
document.getElementById("collapseThirdRow"),
{ toggle: false },
);
collapseThirdRow.hide();
var collapseFourthRow = new bootstrap.Collapse(
document.getElementById("collapseFourthRow"),
{ toggle: false },
);
collapseFourthRow.hide();
}
}
return {
dxcallsign,
busy_state,
arq_state,
new_frequency,
frequency,
mode,
bandwidth,
dbfs_level,
dbfs_level_percent,
speed_level,
fft,
channel_busy,
channel_busy_slot,
getChannelBusySlotState,
scatter,
ptt_state,
s_meter_strength_percent,
s_meter_strength_raw,
arq_total_bytes,
audio_recording,
hamlib_status,
audio_level,
alc,
updateTncState,
arq_transmission_percent,
arq_speed_list_bpm,
arq_speed_list_timestamp,
arq_speed_list_snr,
arq_seconds_until_finish,
arq_seconds_until_timeout,
arq_seconds_until_timeout_percent,
modem_running_state,
arq_session_state,
is_codec2_traffic,
rf_level,
heard_stations,
beacon_state,
rigctld_started,
rigctld_process,
python_version,
modem_version,
rx_buffer_length,
};
});

0
gui/src/style.css Normal file
View file

View file

@ -107,3 +107,11 @@ https://stackoverflow.com/a/9622873
[data-bs-theme="dark"] {
/* default dark theme mods */
}
.modal-backdrop {
background-color: transparent;
}
.modal-backdrop.in {
filter: alpha(opacity=10);
opacity: .1
}

15
gui/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1,15 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
interface ImportMetaEnv {
readonly PACKAGE_VERSION: string;
// more env variables...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View file

@ -1,149 +0,0 @@
#!/usr/bin/env python
# Copyright (c) 2019 Jeppe Ledet-Pedersen
# This software is released under the MIT license.
# See the LICENSE file for further details.
import sys
import json
import argparse
from gnuradio import gr
from gnuradio import uhd
from gnuradio.fft import logpwrfft
import numpy as np
from gevent.pywsgi import WSGIServer
from geventwebsocket import WebSocketError
from geventwebsocket.handler import WebSocketHandler
from bottle import request, Bottle, abort, static_file
app = Bottle()
connections = set()
opts = {}
@app.route('/websocket')
def handle_websocket():
wsock = request.environ.get('wsgi.websocket')
if not wsock:
abort(400, 'Expected WebSocket request.')
connections.add(wsock)
# Send center frequency and span
wsock.send(json.dumps(opts))
while True:
try:
wsock.receive()
except WebSocketError:
break
connections.remove(wsock)
@app.route('/')
def index():
return static_file('index.html', root='.')
@app.route('/<filename>')
def static(filename):
return static_file(filename, root='.')
class fft_broadcast_sink(gr.sync_block):
def __init__(self, fft_size):
gr.sync_block.__init__(self,
name="plotter",
in_sig=[(np.float32, fft_size)],
out_sig=[])
def work(self, input_items, output_items):
ninput_items = len(input_items[0])
for bins in input_items[0]:
p = np.around(bins).astype(int)
p = np.fft.fftshift(p)
for c in connections.copy():
try:
c.send(json.dumps({'s': p.tolist()}, separators=(',', ':')))
except Exception:
connections.remove(c)
self.consume(0, ninput_items)
return 0
class fft_receiver(gr.top_block):
def __init__(self, samp_rate, freq, gain, fft_size, framerate):
gr.top_block.__init__(self, "Top Block")
self.usrp = uhd.usrp_source(
",".join(("", "")),
uhd.stream_args(
cpu_format="fc32",
channels=range(1),
),
)
self.usrp.set_samp_rate(samp_rate)
self.usrp.set_center_freq(freq, 0)
self.usrp.set_gain(gain, 0)
self.fft = logpwrfft.logpwrfft_c(
sample_rate=samp_rate,
fft_size=fft_size,
ref_scale=1,
frame_rate=framerate,
avg_alpha=1,
average=False,
)
self.fft_broadcast = fft_broadcast_sink(fft_size)
self.connect((self.fft, 0), (self.fft_broadcast, 0))
self.connect((self.usrp, 0), (self.fft, 0))
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-s', '--sample-rate', type=float, default=40e6)
parser.add_argument('-f', '--frequency', type=float, default=940e6)
parser.add_argument('-g', '--gain', type=float, default=40)
parser.add_argument('-n', '--fft-size', type=int, default=4096)
parser.add_argument('-r', '--frame-rate', type=int, default=25)
args = parser.parse_args()
if gr.enable_realtime_scheduling() != gr.RT_OK or 0:
print("Error: failed to enable real-time scheduling.")
tb = fft_receiver(
samp_rate=args.sample_rate,
freq=args.frequency,
gain=args.gain,
fft_size=args.fft_size,
framerate=args.frame_rate
)
tb.start()
opts['center'] = args.frequency
opts['span'] = args.sample_rate
server = WSGIServer(("0.0.0.0", 8000), app,
handler_class=WebSocketHandler)
try:
server.serve_forever()
except Exception:
sys.exit(0)
tb.stop()
tb.wait()
if __name__ == '__main__':
main()

View file

@ -1,607 +0,0 @@
/*=============================================================
Filename: Spectrogram-1v00.js
JavaScript graphics functions to draw Spectrograms.
Date Description By
-------|-------------------------------------------------|---
12Nov18 First beta ARC
17Nov18 Added offset into data buffer ARC
08May19 this.imageURL URL added
bugfix: fixed isNaN test
Changed sgStart, sgStop to start, stop
Added options object to constructors ARC
10May19 Enabled Left to Right as well as Top to Bottom ARC
11May19 Added RasterscanSVG ARC
12May19 Added blnkline for horizontal ratser scans ARC
13May19 Eliminated unneccessary putImageData ARC
14May19 Removed toDataURL, not used drawImage is better
bugfix: SVG RHC names swapped ARC
02Jun19 bugfix: startOfs not honored in horizontalNewLine ARC
03Jun19 Flipped the SVG and RHC names for waterfalls ARC
04Jun19 Unflip SVG and RHC for horizontal mode ARC
Swap "SVG" & "RHC" strings to match fn names ARC
05Jun19 bugfix: WaterfallSVG scrolling wrong way ARC
10Jun19 bugfix: support lineRate=0 for static display
bugfix: ipBufPtr must be a ptr to a ptr ARC
11Jun19 Make ipBuffers an Array of Arrays, if lineRate=0
use all buffers else use only ipBuffer[0] ARC
13Jun19 Use Waterfall and Rasterscan plus direction
Use Boolean rater than string compare ARC
16Jun19 Use const and let ARC
20Jun19 Change order of parameters ARC
21Jun19 Add setLineRate method ARC
06Jul19 Released as Rev 1v00 ARC
==============================================================*/
var Waterfall, Rasterscan;
(function () {
Waterfall = function (ipBufAry, w, h, dir, options) {
var direction = typeof dir === "string" ? dir.toLowerCase() : "down";
switch (direction) {
case "up":
return new Spectrogram(ipBufAry, w, h, "WF", false, true, options);
case "down":
default:
return new Spectrogram(ipBufAry, w, h, "WF", true, true, options);
case "left":
return new Spectrogram(ipBufAry, w, h, "WF", false, false, options);
case "right":
return new Spectrogram(ipBufAry, w, h, "WF", true, false, options);
}
};
Rasterscan = function (ipBufAry, w, h, dir, options) {
const direction = typeof dir === "string" ? dir.toLowerCase() : "down";
switch (direction) {
case "up":
return new Spectrogram(ipBufAry, w, h, "RS", true, true, options);
case "down":
default:
return new Spectrogram(ipBufAry, w, h, "RS", false, true, options);
case "left":
return new Spectrogram(ipBufAry, w, h, "RS", false, false, options);
case "right":
return new Spectrogram(ipBufAry, w, h, "RS", true, false, options);
}
};
function Spectrogram(ipBufAry, w, h, sgMode, rhc, vert, options) {
const opt = typeof options === "object" ? options : {}; // avoid undeclared object errors
let offScreenCtx; // offscreen canvas drawing context
const pxPerLine = w || 200;
const lines = h || 200;
let lineRate = 30; // requested line rate for dynamic waterfalls
let interval = 0; // msec
let startOfs = 0;
const lineBuf = new ArrayBuffer(pxPerLine * 4); // 1 line
const lineBuf8 = new Uint8ClampedArray(lineBuf);
const lineImgData = new ImageData(lineBuf8, pxPerLine, 1); // 1 line of canvas pixels
let pageImgData; // lines * pxPerLine of canvas pixels
let ipBuf8; // map input data to 0..255 unsigned bytes
const blankBuf = new ArrayBuffer(pxPerLine * 4); // 1 line
const blankBuf8 = new Uint8ClampedArray(blankBuf);
const blankImgData = new ImageData(blankBuf8, pxPerLine, 1); // 1 line of canvas pixels
const clearBuf = new ArrayBuffer(pxPerLine * lines * 4); // fills with 0s ie. rgba 0,0,0,0 = transparent
const clearBuf8 = new Uint8ClampedArray(clearBuf);
let clearImgData;
let nextLine = 0;
let timerID = null;
let running = false;
let sgTime = 0;
let sgStartTime = 0;
// Matlab Jet ref: stackoverflow.com grayscale-to-red-green-blue-matlab-jet-color-scale
let colMap = [
[0, 0, 128, 255],
[0, 0, 131, 255],
[0, 0, 135, 255],
[0, 0, 139, 255],
[0, 0, 143, 255],
[0, 0, 147, 255],
[0, 0, 151, 255],
[0, 0, 155, 255],
[0, 0, 159, 255],
[0, 0, 163, 255],
[0, 0, 167, 255],
[0, 0, 171, 255],
[0, 0, 175, 255],
[0, 0, 179, 255],
[0, 0, 183, 255],
[0, 0, 187, 255],
[0, 0, 191, 255],
[0, 0, 195, 255],
[0, 0, 199, 255],
[0, 0, 203, 255],
[0, 0, 207, 255],
[0, 0, 211, 255],
[0, 0, 215, 255],
[0, 0, 219, 255],
[0, 0, 223, 255],
[0, 0, 227, 255],
[0, 0, 231, 255],
[0, 0, 235, 255],
[0, 0, 239, 255],
[0, 0, 243, 255],
[0, 0, 247, 255],
[0, 0, 251, 255],
[0, 0, 255, 255],
[0, 4, 255, 255],
[0, 8, 255, 255],
[0, 12, 255, 255],
[0, 16, 255, 255],
[0, 20, 255, 255],
[0, 24, 255, 255],
[0, 28, 255, 255],
[0, 32, 255, 255],
[0, 36, 255, 255],
[0, 40, 255, 255],
[0, 44, 255, 255],
[0, 48, 255, 255],
[0, 52, 255, 255],
[0, 56, 255, 255],
[0, 60, 255, 255],
[0, 64, 255, 255],
[0, 68, 255, 255],
[0, 72, 255, 255],
[0, 76, 255, 255],
[0, 80, 255, 255],
[0, 84, 255, 255],
[0, 88, 255, 255],
[0, 92, 255, 255],
[0, 96, 255, 255],
[0, 100, 255, 255],
[0, 104, 255, 255],
[0, 108, 255, 255],
[0, 112, 255, 255],
[0, 116, 255, 255],
[0, 120, 255, 255],
[0, 124, 255, 255],
[0, 128, 255, 255],
[0, 131, 255, 255],
[0, 135, 255, 255],
[0, 139, 255, 255],
[0, 143, 255, 255],
[0, 147, 255, 255],
[0, 151, 255, 255],
[0, 155, 255, 255],
[0, 159, 255, 255],
[0, 163, 255, 255],
[0, 167, 255, 255],
[0, 171, 255, 255],
[0, 175, 255, 255],
[0, 179, 255, 255],
[0, 183, 255, 255],
[0, 187, 255, 255],
[0, 191, 255, 255],
[0, 195, 255, 255],
[0, 199, 255, 255],
[0, 203, 255, 255],
[0, 207, 255, 255],
[0, 211, 255, 255],
[0, 215, 255, 255],
[0, 219, 255, 255],
[0, 223, 255, 255],
[0, 227, 255, 255],
[0, 231, 255, 255],
[0, 235, 255, 255],
[0, 239, 255, 255],
[0, 243, 255, 255],
[0, 247, 255, 255],
[0, 251, 255, 255],
[0, 255, 255, 255],
[4, 255, 251, 255],
[8, 255, 247, 255],
[12, 255, 243, 255],
[16, 255, 239, 255],
[20, 255, 235, 255],
[24, 255, 231, 255],
[28, 255, 227, 255],
[32, 255, 223, 255],
[36, 255, 219, 255],
[40, 255, 215, 255],
[44, 255, 211, 255],
[48, 255, 207, 255],
[52, 255, 203, 255],
[56, 255, 199, 255],
[60, 255, 195, 255],
[64, 255, 191, 255],
[68, 255, 187, 255],
[72, 255, 183, 255],
[76, 255, 179, 255],
[80, 255, 175, 255],
[84, 255, 171, 255],
[88, 255, 167, 255],
[92, 255, 163, 255],
[96, 255, 159, 255],
[100, 255, 155, 255],
[104, 255, 151, 255],
[108, 255, 147, 255],
[112, 255, 143, 255],
[116, 255, 139, 255],
[120, 255, 135, 255],
[124, 255, 131, 255],
[128, 255, 128, 255],
[131, 255, 124, 255],
[135, 255, 120, 255],
[139, 255, 116, 255],
[143, 255, 112, 255],
[147, 255, 108, 255],
[151, 255, 104, 255],
[155, 255, 100, 255],
[159, 255, 96, 255],
[163, 255, 92, 255],
[167, 255, 88, 255],
[171, 255, 84, 255],
[175, 255, 80, 255],
[179, 255, 76, 255],
[183, 255, 72, 255],
[187, 255, 68, 255],
[191, 255, 64, 255],
[195, 255, 60, 255],
[199, 255, 56, 255],
[203, 255, 52, 255],
[207, 255, 48, 255],
[211, 255, 44, 255],
[215, 255, 40, 255],
[219, 255, 36, 255],
[223, 255, 32, 255],
[227, 255, 28, 255],
[231, 255, 24, 255],
[235, 255, 20, 255],
[239, 255, 16, 255],
[243, 255, 12, 255],
[247, 255, 8, 255],
[251, 255, 4, 255],
[255, 255, 0, 255],
[255, 251, 0, 255],
[255, 247, 0, 255],
[255, 243, 0, 255],
[255, 239, 0, 255],
[255, 235, 0, 255],
[255, 231, 0, 255],
[255, 227, 0, 255],
[255, 223, 0, 255],
[255, 219, 0, 255],
[255, 215, 0, 255],
[255, 211, 0, 255],
[255, 207, 0, 255],
[255, 203, 0, 255],
[255, 199, 0, 255],
[255, 195, 0, 255],
[255, 191, 0, 255],
[255, 187, 0, 255],
[255, 183, 0, 255],
[255, 179, 0, 255],
[255, 175, 0, 255],
[255, 171, 0, 255],
[255, 167, 0, 255],
[255, 163, 0, 255],
[255, 159, 0, 255],
[255, 155, 0, 255],
[255, 151, 0, 255],
[255, 147, 0, 255],
[255, 143, 0, 255],
[255, 139, 0, 255],
[255, 135, 0, 255],
[255, 131, 0, 255],
[255, 128, 0, 255],
[255, 124, 0, 255],
[255, 120, 0, 255],
[255, 116, 0, 255],
[255, 112, 0, 255],
[255, 108, 0, 255],
[255, 104, 0, 255],
[255, 100, 0, 255],
[255, 96, 0, 255],
[255, 92, 0, 255],
[255, 88, 0, 255],
[255, 84, 0, 255],
[255, 80, 0, 255],
[255, 76, 0, 255],
[255, 72, 0, 255],
[255, 68, 0, 255],
[255, 64, 0, 255],
[255, 60, 0, 255],
[255, 56, 0, 255],
[255, 52, 0, 255],
[255, 48, 0, 255],
[255, 44, 0, 255],
[255, 40, 0, 255],
[255, 36, 0, 255],
[255, 32, 0, 255],
[255, 28, 0, 255],
[255, 24, 0, 255],
[255, 20, 0, 255],
[255, 16, 0, 255],
[255, 12, 0, 255],
[255, 8, 0, 255],
[255, 4, 0, 255],
[255, 0, 0, 255],
[251, 0, 0, 255],
[247, 0, 0, 255],
[243, 0, 0, 255],
[239, 0, 0, 255],
[235, 0, 0, 255],
[231, 0, 0, 255],
[227, 0, 0, 255],
[223, 0, 0, 255],
[219, 0, 0, 255],
[215, 0, 0, 255],
[211, 0, 0, 255],
[207, 0, 0, 255],
[203, 0, 0, 255],
[199, 0, 0, 255],
[195, 0, 0, 255],
[191, 0, 0, 255],
[187, 0, 0, 255],
[183, 0, 0, 255],
[179, 0, 0, 255],
[175, 0, 0, 255],
[171, 0, 0, 255],
[167, 0, 0, 255],
[163, 0, 0, 255],
[159, 0, 0, 255],
[155, 0, 0, 255],
[151, 0, 0, 255],
[147, 0, 0, 255],
[143, 0, 0, 255],
[139, 0, 0, 255],
[135, 0, 0, 255],
[131, 0, 0, 255],
[0, 0, 0, 0],
];
function incrLine() {
if ((vert && !rhc) || (!vert && rhc)) {
nextLine++;
if (nextLine >= lines) {
nextLine = 0;
}
} else {
nextLine--;
if (nextLine < 0) {
nextLine = lines - 1;
}
}
}
function updateWaterfall() {
// update dynamic waterfalls at a fixed rate
let sgDiff;
// grab latest line of data, write it to off screen buffer, inc 'nextLine'
sgNewLine();
// loop to write data data at the desired rate, data is being updated asynchronously
// ref for accurate timeout: http://www.sitepoint.com/creating-accurate-timers-in-javascript
sgTime += interval;
sgDiff = Date.now() - sgStartTime - sgTime;
if (running) {
timerID = setTimeout(updateWaterfall, interval - sgDiff);
}
}
function sgSetLineRate(newRate) {
if (isNaN(newRate) || newRate > 50 || newRate < 0) {
console.error("invalid line rate [0 <= lineRate < 50 lines/sec]");
// don't change the lineRate;
} else if (newRate === 0) {
// static (one pass) raster
lineRate = 0;
} else {
lineRate = newRate;
interval = 1000 / lineRate; // msec
}
}
this.setLineRate = sgSetLineRate;
function setProperty(propertyName, value) {
if (typeof propertyName !== "string" || value === undefined) {
// null is OK, forces default
return;
}
switch (propertyName.toLowerCase()) {
case "linerate":
sgSetLineRate(value); // setLine does checks for number etc
break;
case "startbin":
if (!isNaN(value) && value > 0) {
startOfs = value;
}
break;
case "onscreenparentid":
if (typeof value === "string" && document.getElementById(value)) {
demoCvsId = value;
}
break;
case "colormap":
if (
Array.isArray(value) &&
Array.isArray(value[0]) &&
value[0].length == 4
) {
colMap = value; // value must be an array of 4 element arrays to get here
if (colMap.length < 256) {
// fill out the remaining colors with last color
for (let i = colMap.length; i < 256; i++) {
colMap[i] = colMap[colMap.length - 1];
}
}
}
break;
default:
break;
}
}
function verticalNewLine() {
let tmpImgData, ipBuf8;
if (sgMode == "WF") {
if (rhc) {
// shift the current display down 1 line, oldest line drops off
tmpImgData = offScreenCtx.getImageData(0, 0, pxPerLine, lines - 1);
offScreenCtx.putImageData(tmpImgData, 0, 1);
} else {
// shift the current display up 1 line, oldest line drops off
tmpImgData = offScreenCtx.getImageData(0, 1, pxPerLine, lines - 1);
offScreenCtx.putImageData(tmpImgData, 0, 0);
}
}
ipBuf8 = Uint8ClampedArray.from(ipBufAry[0]);
for (
let sigVal, rgba, opIdx = 0, ipIdx = startOfs;
ipIdx < pxPerLine + startOfs;
opIdx += 4, ipIdx++
) {
sigVal = ipBuf8[ipIdx] || 0; // if input line too short add zeros
rgba = colMap[sigVal]; // array of rgba values
// byte reverse so number aa bb gg rr
lineBuf8[opIdx] = rgba[0]; // red
lineBuf8[opIdx + 1] = rgba[1]; // green
lineBuf8[opIdx + 2] = rgba[2]; // blue
lineBuf8[opIdx + 3] = rgba[3]; // alpha
}
offScreenCtx.putImageData(lineImgData, 0, nextLine);
if (sgMode === "RS") {
incrLine();
// if not static draw a white line in front of the current line to indicate new data point
if (lineRate) {
offScreenCtx.putImageData(blankImgData, 0, nextLine);
}
}
}
function horizontalNewLine() {
let tmpImgData, ipBuf8;
if (sgMode == "WF") {
if (rhc) {
// shift the current display right 1 line, oldest line drops off
tmpImgData = offScreenCtx.getImageData(0, 0, lines - 1, pxPerLine);
offScreenCtx.putImageData(tmpImgData, 1, 0);
} else {
// shift the current display left 1 line, oldest line drops off
tmpImgData = offScreenCtx.getImageData(1, 0, lines - 1, pxPerLine);
offScreenCtx.putImageData(tmpImgData, 0, 0);
}
}
// refresh the page image (it was just shifted)
pageImgData = offScreenCtx.getImageData(0, 0, lines, pxPerLine);
if (ipBufAry[0].constructor !== Uint8Array) {
ipBuf8 = Uint8ClampedArray.from(ipBufAry[0]); // clamp input values to 0..255 range
} else {
ipBuf8 = ipBufAry[0]; // conversion already done
}
for (let sigVal, rgba, opIdx, ipIdx = 0; ipIdx < pxPerLine; ipIdx++) {
sigVal = ipBuf8[ipIdx + startOfs] || 0; // if input line too short add zeros
rgba = colMap[sigVal]; // array of rgba values
opIdx = 4 * ((pxPerLine - ipIdx - 1) * lines + nextLine);
// byte reverse so number aa bb gg rr
pageImgData.data[opIdx] = rgba[0]; // red
pageImgData.data[opIdx + 1] = rgba[1]; // green
pageImgData.data[opIdx + 2] = rgba[2]; // blue
pageImgData.data[opIdx + 3] = rgba[3]; // alpha
}
if (sgMode === "RS") {
incrLine();
// if not draw a white line in front of the current line to indicate new data point
if (lineRate) {
for (let j = 0; j < pxPerLine; j++) {
if (rhc) {
opIdx = 4 * (j * lines + nextLine);
} else {
opIdx = 4 * ((pxPerLine - j - 1) * lines + nextLine);
}
// byte reverse so number aa bb gg rr
pageImgData.data[opIdx] = 255; // red
pageImgData.data[opIdx + 1] = 255; // green
pageImgData.data[opIdx + 2] = 255; // blue
pageImgData.data[opIdx + 3] = 255; // alpha
}
}
}
offScreenCtx.putImageData(pageImgData, 0, 0);
}
const sgNewLine = vert ? verticalNewLine : horizontalNewLine; // function pointers
//===== set all the options ================
for (let prop in opt) {
// check that this is opt's own property, not inherited from prototype
if (opt.hasOwnProperty(prop)) {
setProperty(prop, opt[prop]);
}
}
// ===== now make the exposed properties and methods ===============
this.newLine = sgNewLine;
this.offScreenCvs = document.createElement("canvas");
if (vert) {
this.offScreenCvs.setAttribute("width", pxPerLine); // reset canvas pixels width
this.offScreenCvs.setAttribute("height", lines); // don't use style for this
clearImgData = new ImageData(clearBuf8, pxPerLine, lines);
} // data written in columns
else {
this.offScreenCvs.setAttribute("width", lines); // reset canvas pixels width
this.offScreenCvs.setAttribute("height", pxPerLine); // don't use style for this
clearImgData = new ImageData(clearBuf8, lines, pxPerLine);
}
offScreenCtx = this.offScreenCvs.getContext("2d");
this.clear = function () {
offScreenCtx.putImageData(clearImgData, 0, 0);
};
this.start = function () {
sgStartTime = Date.now();
sgTime = 0;
running = true;
updateWaterfall(); // start the update loop
};
this.stop = function () {
running = false;
if (timerID) {
clearTimeout(timerID);
}
// reset where the next line is to be written
if (sgMode === "RS") {
if (vert) {
nextLine = rhc ? lines - 1 : 0;
} else {
nextLine = rhc ? 0 : lines - 1;
}
} // WF
else {
nextLine = rhc ? 0 : lines - 1;
}
};
// make a white line, it will show the input line for RS displays
blankBuf8.fill(255);
// make a full canvas of the color map 0 values
for (let i = 0; i < pxPerLine * lines * 4; i += 4) {
// byte reverse so number aa bb gg rr
clearBuf8[i] = colMap[0][0]; // red
clearBuf8[i + 1] = colMap[0][1]; // green
clearBuf8[i + 2] = colMap[0][2]; // blue
clearBuf8[i + 3] = colMap[0][3]; // alpha
}
// for diagnostics only
if (typeof demoCvsId == "string") {
document.getElementById(demoCvsId).appendChild(this.offScreenCvs);
}
// initialize the direction and first line position
this.stop();
// everything is set
// if dynamic, wait for the start or newLine methods to be called
}
})();

View file

@ -1,488 +0,0 @@
/*
* Copyright (c) 2019 Jeppe Ledet-Pedersen
* This software is released under the MIT license.
* See the LICENSE file for further details.
*/
"use strict";
Spectrum.prototype.squeeze = function (value, out_min, out_max) {
if (value <= this.min_db) return out_min;
else if (value >= this.max_db) return out_max;
else
return Math.round(
((value - this.min_db) / (this.max_db - this.min_db)) * out_max
);
};
Spectrum.prototype.rowToImageData = function (bins) {
for (var i = 0; i < this.imagedata.data.length; i += 4) {
var cindex = this.squeeze(bins[i / 4], 0, 255);
var color = this.colormap[cindex];
this.imagedata.data[i + 0] = color[0];
this.imagedata.data[i + 1] = color[1];
this.imagedata.data[i + 2] = color[2];
this.imagedata.data[i + 3] = 255;
}
};
Spectrum.prototype.addWaterfallRow = function (bins) {
// Shift waterfall 1 row down
this.ctx_wf.drawImage(
this.ctx_wf.canvas,
0,
0,
this.wf_size,
this.wf_rows - 1,
0,
1,
this.wf_size,
this.wf_rows - 1
);
// Draw new line on waterfall canvas
this.rowToImageData(bins);
this.ctx_wf.putImageData(this.imagedata, 0, 0);
var width = this.ctx.canvas.width;
var height = this.ctx.canvas.height;
// Copy scaled FFT canvas to screen. Only copy the number of rows that will
// fit in waterfall area to avoid vertical scaling.
this.ctx.imageSmoothingEnabled = false;
var rows = Math.min(this.wf_rows, height - this.spectrumHeight);
this.ctx.drawImage(
this.ctx_wf.canvas,
0,
0,
this.wf_size,
rows,
0,
this.spectrumHeight,
width,
height - this.spectrumHeight
);
};
Spectrum.prototype.drawFFT = function (bins) {
this.ctx.beginPath();
this.ctx.moveTo(-1, this.spectrumHeight + 1);
for (var i = 0; i < bins.length; i++) {
var y = this.spectrumHeight - this.squeeze(bins[i], 0, this.spectrumHeight);
if (y > this.spectrumHeight - 1) y = this.spectrumHeight + 1; // Hide underflow
if (y < 0) y = 0;
if (i == 0) this.ctx.lineTo(-1, y);
this.ctx.lineTo(i, y);
if (i == bins.length - 1) this.ctx.lineTo(this.wf_size + 1, y);
}
this.ctx.lineTo(this.wf_size + 1, this.spectrumHeight + 1);
this.ctx.strokeStyle = "#fefefe";
this.ctx.stroke();
};
//Spectrum.prototype.drawSpectrum = function(bins) {
Spectrum.prototype.drawSpectrum = function () {
var width = this.ctx.canvas.width;
var height = this.ctx.canvas.height;
// Modification by DJ2LS
// Draw bandwidth lines
// TODO: Math not correct. But a first attempt
// it seems position is more or less equal to frequenzy by factor 10
// eg. position 150 == 1500Hz
/*
// CENTER LINE
this.ctx_wf.beginPath();
this.ctx_wf.moveTo(150,0);
this.ctx_wf.lineTo(150, height);
this.ctx_wf.lineWidth = 1;
this.ctx_wf.strokeStyle = '#8C8C8C';
this.ctx_wf.stroke()
*/
// 586Hz and 1700Hz LINES
var linePositionLow = 121.6; //150 - bandwith/20
var linePositionHigh = 178.4; //150 + bandwidth/20
var linePositionLow2 = 65; //150 - bandwith/20
var linePositionHigh2 = 235; //150 + bandwith/20
this.ctx_wf.beginPath();
this.ctx_wf.moveTo(linePositionLow, 0);
this.ctx_wf.lineTo(linePositionLow, height);
this.ctx_wf.moveTo(linePositionHigh, 0);
this.ctx_wf.lineTo(linePositionHigh, height);
this.ctx_wf.moveTo(linePositionLow2, 0);
this.ctx_wf.lineTo(linePositionLow2, height);
this.ctx_wf.moveTo(linePositionHigh2, 0);
this.ctx_wf.lineTo(linePositionHigh2, height);
this.ctx_wf.lineWidth = 1;
this.ctx_wf.strokeStyle = "#C3C3C3";
this.ctx_wf.stroke();
// ---- END OF MODIFICATION ------
// Fill with black
this.ctx.fillStyle = "white";
this.ctx.fillRect(0, 0, width, height);
//Commenting out the remainder of this code, it's not needed and unused as of 6.9.11 and saves three if statements
return;
/*
// FFT averaging
if (this.averaging > 0) {
if (!this.binsAverage || this.binsAverage.length != bins.length) {
this.binsAverage = Array.from(bins);
} else {
for (var i = 0; i < bins.length; i++) {
this.binsAverage[i] += this.alpha * (bins[i] - this.binsAverage[i]);
}
}
bins = this.binsAverage;
}
// Max hold
if (this.maxHold) {
if (!this.binsMax || this.binsMax.length != bins.length) {
this.binsMax = Array.from(bins);
} else {
for (var i = 0; i < bins.length; i++) {
if (bins[i] > this.binsMax[i]) {
this.binsMax[i] = bins[i];
} else {
// Decay
this.binsMax[i] = 1.0025 * this.binsMax[i];
}
}
}
}
// Do not draw anything if spectrum is not visible
if (this.ctx_axes.canvas.height < 1)
return;
// Scale for FFT
this.ctx.save();
this.ctx.scale(width / this.wf_size, 1);
// Draw maxhold
if (this.maxHold)
this.drawFFT(this.binsMax);
// Draw FFT bins
this.drawFFT(bins);
// Restore scale
this.ctx.restore();
// Fill scaled path
this.ctx.fillStyle = this.gradient;
this.ctx.fill();
// Copy axes from offscreen canvas
this.ctx.drawImage(this.ctx_axes.canvas, 0, 0);
*/
};
//Allow setting colormap
Spectrum.prototype.setColorMap = function (index) {
this.colormap = colormaps[index];
};
Spectrum.prototype.updateAxes = function () {
var width = this.ctx_axes.canvas.width;
var height = this.ctx_axes.canvas.height;
// Clear axes canvas
this.ctx_axes.clearRect(0, 0, width, height);
// Draw axes
this.ctx_axes.font = "12px sans-serif";
this.ctx_axes.fillStyle = "white";
this.ctx_axes.textBaseline = "middle";
this.ctx_axes.textAlign = "left";
var step = 10;
for (var i = this.min_db + 10; i <= this.max_db - 10; i += step) {
var y = height - this.squeeze(i, 0, height);
this.ctx_axes.fillText(i, 5, y);
this.ctx_axes.beginPath();
this.ctx_axes.moveTo(20, y);
this.ctx_axes.lineTo(width, y);
this.ctx_axes.strokeStyle = "rgba(200, 200, 200, 0.10)";
this.ctx_axes.stroke();
}
this.ctx_axes.textBaseline = "bottom";
for (var i = 0; i < 11; i++) {
var x = Math.round(width / 10) * i;
if (this.spanHz > 0) {
var adjust = 0;
if (i == 0) {
this.ctx_axes.textAlign = "left";
adjust = 3;
} else if (i == 10) {
this.ctx_axes.textAlign = "right";
adjust = -3;
} else {
this.ctx_axes.textAlign = "center";
}
var freq = this.centerHz + (this.spanHz / 10) * (i - 5);
if (this.centerHz + this.spanHz > 1e6) freq = freq / 1e6 + "M";
else if (this.centerHz + this.spanHz > 1e3) freq = freq / 1e3 + "k";
this.ctx_axes.fillText(freq, x + adjust, height - 3);
}
this.ctx_axes.beginPath();
this.ctx_axes.moveTo(x, 0);
this.ctx_axes.lineTo(x, height);
this.ctx_axes.strokeStyle = "rgba(200, 200, 200, 0.10)";
this.ctx_axes.stroke();
}
};
Spectrum.prototype.addData = function (data) {
if (!this.paused) {
if (data.length != this.wf_size) {
this.wf_size = data.length;
this.ctx_wf.canvas.width = data.length;
this.ctx_wf.fillStyle = "white";
this.ctx_wf.fillRect(0, 0, this.wf.width, this.wf.height);
this.imagedata = this.ctx_wf.createImageData(data.length, 1);
}
//this.drawSpectrum(data);
this.drawSpectrum();
this.addWaterfallRow(data);
this.resize();
}
};
Spectrum.prototype.updateSpectrumRatio = function () {
this.spectrumHeight = Math.round(
(this.canvas.height * this.spectrumPercent) / 100.0
);
this.gradient = this.ctx.createLinearGradient(0, 0, 0, this.spectrumHeight);
for (var i = 0; i < this.colormap.length; i++) {
var c = this.colormap[this.colormap.length - 1 - i];
this.gradient.addColorStop(
i / this.colormap.length,
"rgba(" + c[0] + "," + c[1] + "," + c[2] + ", 1.0)"
);
}
};
Spectrum.prototype.resize = function () {
var width = this.canvas.clientWidth;
var height = this.canvas.clientHeight;
if (this.canvas.width != width || this.canvas.height != height) {
this.canvas.width = width;
this.canvas.height = height;
this.updateSpectrumRatio();
}
if (this.axes.width != width || this.axes.height != this.spectrumHeight) {
this.axes.width = width;
this.axes.height = this.spectrumHeight;
this.updateAxes();
}
};
Spectrum.prototype.setSpectrumPercent = function (percent) {
if (percent >= 0 && percent <= 100) {
this.spectrumPercent = percent;
this.updateSpectrumRatio();
}
};
Spectrum.prototype.incrementSpectrumPercent = function () {
if (this.spectrumPercent + this.spectrumPercentStep <= 100) {
this.setSpectrumPercent(this.spectrumPercent + this.spectrumPercentStep);
}
};
Spectrum.prototype.decrementSpectrumPercent = function () {
if (this.spectrumPercent - this.spectrumPercentStep >= 0) {
this.setSpectrumPercent(this.spectrumPercent - this.spectrumPercentStep);
}
};
Spectrum.prototype.toggleColor = function () {
this.colorindex++;
if (this.colorindex >= colormaps.length) this.colorindex = 0;
this.colormap = colormaps[this.colorindex];
this.updateSpectrumRatio();
};
Spectrum.prototype.setRange = function (min_db, max_db) {
this.min_db = min_db;
this.max_db = max_db;
this.updateAxes();
};
Spectrum.prototype.rangeUp = function () {
this.setRange(this.min_db - 5, this.max_db - 5);
};
Spectrum.prototype.rangeDown = function () {
this.setRange(this.min_db + 5, this.max_db + 5);
};
Spectrum.prototype.rangeIncrease = function () {
this.setRange(this.min_db - 5, this.max_db + 5);
};
Spectrum.prototype.rangeDecrease = function () {
if (this.max_db - this.min_db > 10)
this.setRange(this.min_db + 5, this.max_db - 5);
};
Spectrum.prototype.setCenterHz = function (hz) {
this.centerHz = hz;
this.updateAxes();
};
Spectrum.prototype.setSpanHz = function (hz) {
this.spanHz = hz;
this.updateAxes();
};
Spectrum.prototype.setAveraging = function (num) {
if (num >= 0) {
this.averaging = num;
this.alpha = 2 / (this.averaging + 1);
}
};
Spectrum.prototype.incrementAveraging = function () {
this.setAveraging(this.averaging + 1);
};
Spectrum.prototype.decrementAveraging = function () {
if (this.averaging > 0) {
this.setAveraging(this.averaging - 1);
}
};
Spectrum.prototype.setPaused = function (paused) {
this.paused = paused;
};
Spectrum.prototype.togglePaused = function () {
this.setPaused(!this.paused);
};
Spectrum.prototype.setMaxHold = function (maxhold) {
this.maxHold = maxhold;
this.binsMax = undefined;
};
Spectrum.prototype.toggleMaxHold = function () {
this.setMaxHold(!this.maxHold);
};
Spectrum.prototype.toggleFullscreen = function () {
if (!this.fullscreen) {
if (this.canvas.requestFullscreen) {
this.canvas.requestFullscreen();
} else if (this.canvas.mozRequestFullScreen) {
this.canvas.mozRequestFullScreen();
} else if (this.canvas.webkitRequestFullscreen) {
this.canvas.webkitRequestFullscreen();
} else if (this.canvas.msRequestFullscreen) {
this.canvas.msRequestFullscreen();
}
this.fullscreen = true;
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
this.fullscreen = false;
}
};
Spectrum.prototype.onKeypress = function (e) {
if (e.key == " ") {
this.togglePaused();
} else if (e.key == "f") {
this.toggleFullscreen();
} else if (e.key == "c") {
this.toggleColor();
} else if (e.key == "ArrowUp") {
this.rangeUp();
} else if (e.key == "ArrowDown") {
this.rangeDown();
} else if (e.key == "ArrowLeft") {
this.rangeDecrease();
} else if (e.key == "ArrowRight") {
this.rangeIncrease();
} else if (e.key == "s") {
this.incrementSpectrumPercent();
} else if (e.key == "w") {
this.decrementSpectrumPercent();
} else if (e.key == "+") {
this.incrementAveraging();
} else if (e.key == "-") {
this.decrementAveraging();
} else if (e.key == "m") {
this.toggleMaxHold();
}
};
function Spectrum(id, options) {
// Handle options
this.centerHz = options && options.centerHz ? options.centerHz : 1500;
this.spanHz = options && options.spanHz ? options.spanHz : 0;
this.wf_size = options && options.wf_size ? options.wf_size : 0;
this.wf_rows = options && options.wf_rows ? options.wf_rows : 1024;
this.spectrumPercent =
options && options.spectrumPercent ? options.spectrumPercent : 0;
this.spectrumPercentStep =
options && options.spectrumPercentStep ? options.spectrumPercentStep : 0;
this.averaging = options && options.averaging ? options.averaging : 0;
this.maxHold = options && options.maxHold ? options.maxHold : false;
// Setup state
this.paused = false;
this.fullscreen = false;
this.min_db = 0;
this.max_db = 70;
this.spectrumHeight = 0;
// Colors
this.colorindex = 0;
this.colormap = colormaps[2];
// Create main canvas and adjust dimensions to match actual
this.canvas = document.getElementById(id);
this.canvas.height = this.canvas.clientHeight;
this.canvas.width = this.canvas.clientWidth;
this.ctx = this.canvas.getContext("2d");
this.ctx.fillStyle = "white";
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Create offscreen canvas for axes
this.axes = document.createElement("canvas");
this.axes.height = 1; // Updated later
this.axes.width = this.canvas.width;
this.ctx_axes = this.axes.getContext("2d");
// Create offscreen canvas for waterfall
this.wf = document.createElement("canvas");
this.wf.height = this.wf_rows;
this.wf.width = this.wf_size;
this.ctx_wf = this.wf.getContext("2d");
// Trigger first render
this.setAveraging(this.averaging);
this.updateSpectrumRatio();
this.resize();
}

39
gui/tsconfig.json Normal file
View file

@ -0,0 +1,39 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": false,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noEmit": true,
"allowJs": true,
"noImplicitAny": false
},
"include": ["src"],
"exclude": ["node_modules"],
"references": [
{ "path": "./tsconfig.node.json" }
]
}

10
gui/tsconfig.node.json Normal file
View file

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts", "package.json", "electron"]
}

94
gui/vite.config.ts Normal file
View file

@ -0,0 +1,94 @@
import { rmSync } from "node:fs";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import electron from "vite-plugin-electron";
import renderer from "vite-plugin-electron-renderer";
import { notBundle } from "vite-plugin-electron/plugin";
import pkg from "./package.json";
// https://vitejs.dev/config/
export default defineConfig(({ command }) => {
rmSync("dist-electron", { recursive: true, force: true });
const isServe = command === "serve";
const isBuild = command === "build";
const sourcemap = isServe || !!process.env.VSCODE_DEBUG;
return {
plugins: [
vue(),
electron([
{
// Main process entry file of the Electron App.
entry: "electron/main/index.ts",
onstart({ startup }) {
if (process.env.VSCODE_DEBUG) {
console.log(
/* For `.vscode/.debug.script.mjs` */ "[startup] Electron App",
);
} else {
startup();
}
},
vite: {
build: {
sourcemap,
minify: isBuild,
outDir: "dist-electron/main",
rollupOptions: {
// Some third-party Node.js libraries may not be built correctly by Vite, especially `C/C++` addons,
// we can use `external` to exclude them to ensure they work correctly.
// Others need to put them in `dependencies` to ensure they are collected into `app.asar` after the app is built.
// Of course, this is not absolute, just this way is relatively simple. :)
external: Object.keys(
"dependencies" in pkg ? pkg.dependencies : {},
),
},
},
plugins: [
// This is just an option to improve build performance, it's non-deterministic!
// e.g. `import log from 'electron-log'` -> `const log = require('electron-log')`
isServe && notBundle(),
],
},
},
{
entry: "electron/preload/index.ts",
onstart({ reload }) {
// Notify the Renderer process to reload the page when the Preload scripts build is complete,
// instead of restarting the entire Electron App.
reload();
},
vite: {
build: {
sourcemap: sourcemap ? "inline" : undefined, // #332
minify: isBuild,
outDir: "dist-electron/preload",
rollupOptions: {
external: Object.keys(
"dependencies" in pkg ? pkg.dependencies : {},
),
},
},
plugins: [isServe && notBundle()],
},
},
]),
// Use Node.js API in the Renderer process
renderer(),
],
server:
process.env.VSCODE_DEBUG &&
(() => {
const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL);
return {
host: url.hostname,
port: +url.port,
};
})(),
define: {
"import.meta.env.PACKAGE_VERSION": JSON.stringify(pkg.version),
},
clearScreen: false,
};
});

View file

View file

@ -4,7 +4,7 @@ import helpers
import time
import modem
import base64
from static import ARQ, AudioParam, Beacon, Channel, Daemon, HamlibParam, ModemParam, Station, Statistics, TCIParam, TNC
from global_instances import ARQ, AudioParam, Beacon, Channel, Daemon, HamlibParam, ModemParam, Station, Statistics, TCIParam, Modem
import sock
import ujson as json
@ -34,7 +34,7 @@ class broadcastHandler:
modem.RECEIVE_DATAC4 = True
self.send_data_to_socket_queue(
freedata="tnc-message",
freedata="modem-message",
fec="wakeup",
mode=self.wakeup_mode,
bursts=bursts,
@ -42,7 +42,7 @@ class broadcastHandler:
)
self.log.info(
"[TNC] FRAME WAKEUP RCVD ["
"[Modem] FRAME WAKEUP RCVD ["
+ str(self.fec_wakeup_callsign, "UTF-8")
+ "] ", mode=self.wakeup_mode, bursts=bursts,
)
@ -51,13 +51,13 @@ class broadcastHandler:
print(self.fec_wakeup_callsign)
self.send_data_to_socket_queue(
freedata="tnc-message",
freedata="modem-message",
fec="broadcast",
dxcallsign=str(self.fec_wakeup_callsign, "UTF-8"),
data=base64.b64encode(data_in[1:]).decode("UTF-8")
)
self.log.info("[TNC] FEC DATA RCVD")
self.log.info("[Modem] FEC DATA RCVD")
def send_data_to_socket_queue(self, **jsondata):
"""
@ -67,7 +67,7 @@ class broadcastHandler:
Dictionary containing the data to be sent, in the format:
key=value, for each item. E.g.:
self.send_data_to_socket_queue(
freedata="tnc-message",
freedata="modem-message",
arq="received",
status="success",
uuid=self.transmission_uuid,
@ -87,12 +87,12 @@ class broadcastHandler:
if "dxcallsign" not in jsondata:
jsondata["dxcallsign"] = str(Station.dxcallsign, "UTF-8")
except Exception as e:
self.log.debug("[TNC] error adding callsigns to network message", e=e)
self.log.debug("[Modem] error adding callsigns to network message", e=e)
# run json dumps
json_data_out = json.dumps(jsondata)
self.log.debug("[TNC] send_data_to_socket_queue:", jsondata=json_data_out)
self.log.debug("[Modem] send_data_to_socket_queue:", jsondata=json_data_out)
# finally push data to our network queue
sock.SOCKET_QUEUE.put(json_data_out)
@ -106,11 +106,11 @@ class broadcastHandler:
self.broadcast_timeout_reached = True
self.log.info(
"[TNC] closing broadcast slot ["
"[Modem] closing broadcast slot ["
+ str(self.fec_wakeup_callsign, "UTF-8")
+ "] ", mode=self.wakeup_mode, bursts=self.broadcast_payload_bursts,
)
# TODO: We need a dynamic way of modifying this
# TODO We need a dynamic way of modifying this
modem.RECEIVE_DATAC4 = False
self.fec_wakeup_callsign = bytes()
self.wakeup_received = False

View file

@ -0,0 +1 @@
{"DJ2LS-0": "22864b", "IW2DHW-0": "fcef94", "BEACON": "5492c7", "EI7IG-0": "0975c8", "G0HWW-0": "2cb363", "LA3QMA-0": "2b9fac", "EA7KOH-0": "9e1c3e", "OK6MS-0": "5f75ed", "N1QM-0": "e045a0"}

View file

@ -84,7 +84,7 @@ def freedv_get_mode_name_by_value(mode: int) -> str:
#else:
sys.path.append(os.path.abspath("."))
log.info("[C2 ] Searching for libcodec2...")
#log.info("[C2 ] Searching for libcodec2...")
if sys.platform == "linux":
files = glob.glob(r"**/*libcodec2*", recursive=True)
files.append("libcodec2.so")
@ -102,14 +102,14 @@ api = None
for file in files:
try:
api = ctypes.CDLL(file)
log.info("[C2 ] Libcodec2 loaded", path=file)
#log.info("[C2 ] Libcodec2 loaded", path=file)
break
except OSError as err:
log.warning("[C2 ] Libcodec2 found but not loaded", path=file, e=err)
log.warning("[C2 ] Error: Libcodec2 found but not loaded", path=file, e=err)
# Quit module if codec2 cant be loaded
if api is None or "api" not in locals():
log.critical("[C2 ] Libcodec2 not loaded - Exiting")
log.critical("[C2 ] Error: Libcodec2 not loaded - Exiting")
sys.exit(1)
# ctypes function init

View file

@ -45,7 +45,7 @@ class CONFIG:
write entire config
"""
self.config['NETWORK'] = {'#Network settings': None,
'TNCPORT': data[50]
'ModemPORT': data[50]
}
self.config['STATION'] = {'#Station settings': None,
@ -66,7 +66,7 @@ class CONFIG:
'rigctld_ip': data[6],
'rigctld_port': data[7]
}
self.config['TNC'] = {'#TNC settings': None,
self.config['Modem'] = {'#Modem settings': None,
'scatter': data[8],
'fft': data[9],
'narrowband': data[10],

Some files were not shown because too many files have changed in this diff Show more