Merge pull request #522 from DJ2LS/develop

This commit is contained in:
DJ2LS 2024-01-17 13:21:45 +01:00 committed by GitHub
commit 236fb17376
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
218 changed files with 10165 additions and 25513 deletions

View file

@ -7,7 +7,8 @@ updates:
directory: "/"
schedule:
interval: "daily"
target-branch: "develop"
# Maintain dependencies for npm
- package-ecosystem: "npm"
directory: "/gui"
@ -20,3 +21,4 @@ updates:
directory: "/"
schedule:
interval: "daily"
target-branch: "develop"

37
.github/workflows/build_gui.yml vendored Normal file
View file

@ -0,0 +1,37 @@
name: build_gui
on: [push]
jobs:
build_i686_x64_release:
name: Build FreeDATA GUI
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, macos-latest, windows-latest]
include:
- os: ubuntu-20.04
electron_parameters: "-p always"
- os: macos-latest
electron_parameters: "-p always"
- os: windows-latest
electron_parameters: "-p always --x64 --ia32"
steps:
- name: Checkout code for ${{ matrix.platform.name }}
uses: actions/checkout@v4
with:
repository: DJ2LS/FreeDATA
- 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 }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
working-directory: gui
run: |
npm i
npm run release

View file

@ -1,412 +0,0 @@
name: Build_Multiplatform
on: [push]
jobs:
BUILD_AMD64:
name: Build codec2 for x86/x64 devices
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
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:
- os: ubuntu-20.04
libcodec2_name: libcodec2.so.1.2
libcodec2_os_name: libcodec2_ubuntu-2004
libcodec2_filetype: so
generator: Unix Makefiles
shell: bash
- os: ubuntu-22.04
libcodec2_name: libcodec2.so.1.2
libcodec2_os_name: libcodec2_ubuntu-2204
libcodec2_filetype: so
generator: Unix Makefiles
shell: bash
- os: macos-latest
libcodec2_name: libcodec2.1.2.dylib
libcodec2_os_name: libcodec2_macos-latest
libcodec2_filetype: dylib
generator: Unix Makefiles
shell: bash
- os: macos-12
libcodec2_name: libcodec2.1.2.dylib
libcodec2_os_name: libcodec2_macos-12
libcodec2_filetype: dylib
generator: Unix Makefiles
shell: bash
steps:
- name: Build codec2 on ${{ matrix.os }} for ${{ matrix.platform.name }}
if: ${{startsWith(matrix.platform.name, 'native') }}
run: |
git clone https://github.com/drowe67/codec2.git
cd codec2
mkdir build
mkdir tempfiles
cd build
cmake -DCMAKE_BUILD_TYPE=Release ../
make
mv src/${{ matrix.libcodec2_name }} ../tempfiles/libcodec2_${{ matrix.os }}_${{ matrix.platform.name }}.${{ matrix.libcodec2_filetype }}
- name: LIST ALL FILES ${{ github.workspace }}
run: ls -R ${{ github.workspace }}
- uses: actions/upload-artifact@v4
if: ${{startsWith(matrix.platform.name, 'native') }}
with:
name: libcodec2_${{ matrix.os }}_${{ matrix.platform.name }}.${{ matrix.libcodec2_filetype }}
# path: ${{ github.workspace }}/codec2/tempfiles/libcodec2_${{ matrix.os }}_${{ matrix.platform.name }}.${{ matrix.libcodec2_filetype }}
path: ${{ github.workspace }}/codec2/tempfiles/
- name: Build codec2 ${{ matrix.platform.name }} ${{ matrix.architecture }}
if: ${{startsWith(matrix.os, 'ubuntu-20') && !startsWith(matrix.platform.name, 'native') }}
run: |
sudo apt install build-essential mingw-w64 g++-mingw-w64 make cmake
git clone https://github.com/drowe67/codec2.git
cd codec2
mkdir tempfiles
mkdir build_w32
cd build_w32
echo 'set(CMAKE_SYSTEM_NAME ${{ matrix.platform.name }})' > toolchain-ubuntu-mingw32.cmake
echo 'set(CMAKE_C_COMPILER ${{ matrix.architecture }}-gcc)' >> toolchain-ubuntu-mingw32.cmake
echo 'set(CMAKE_CXX_COMPILER ${{ matrix.architecture }}-g++)' >> toolchain-ubuntu-mingw32.cmake
echo 'set(CMAKE_RC_COMPILER ${{ matrix.architecture }}-windres)' >> toolchain-ubuntu-mingw32.cmake
echo 'set(CMAKE_FIND_ROOT_PATH /usr/${{ matrix.architecture }})' >> toolchain-ubuntu-mingw32.cmake
echo 'set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)' >> toolchain-ubuntu-mingw32.cmake
echo 'set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)' >> toolchain-ubuntu-mingw32.cmake
echo 'set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)' >> toolchain-ubuntu-mingw32.cmake
echo 'set(CMAKE_SHARED_LINKER_FLAGS "-static-libgcc -static-libstdc++ -static")' >> toolchain-ubuntu-mingw32.cmake
cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=toolchain-ubuntu-mingw32.cmake ..
make
mv src/libcodec2.${{ matrix.platform.file }} ../tempfiles/libcodec2_${{ matrix.platform.name }}_${{ matrix.architecture }}.${{ matrix.platform.file }}
- uses: actions/upload-artifact@v4
if: ${{startsWith(matrix.os, 'ubuntu-20') && !startsWith(matrix.platform.name, 'native') }}
with:
name: libcodec2_${{ matrix.os }}_${{ matrix.platform.name }}_${{ matrix.architecture }}.${{ matrix.platform.file }}
path: codec2/tempfiles/*
BUILD_ARM:
# The host should always be linux
runs-on: ubuntu-latest
name: Build codec2 for ARM devices
# Run steps on a matrix of 2 arch/distro combinations
strategy:
matrix:
include:
- arch: armv7
distro: bullseye
libcodec2_os_name: libcodec2_bullseye_armv7.so
- arch: armv7
distro: ubuntu_latest
libcodec2_os_name: libcodec2_ubuntu_latest_armv7.so
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
- uses: uraimo/run-on-arch-action@v2
name: Build artifact
id: build
with:
arch: ${{ matrix.arch }}
distro: ${{ matrix.distro }}
# Not required, but speeds up builds
githubToken: ${{ github.token }}
# Create an artifacts directory
setup: |
mkdir -p "${PWD}/artifacts"
# Mount the artifacts directory as /artifacts in the container
dockerRunArgs: |
--volume "${PWD}/artifacts:/artifacts"
# Pass some environment variables to the container
env: | # YAML, but pipe character is necessary
artifact_name: ${{ matrix.libcodec2_os_name }}
# The shell to run commands with in the container
shell: /bin/sh
# Install some dependencies in the container. This speeds up builds if
# you are also using githubToken. Any dependencies installed here will
# be part of the container image that gets cached, so subsequent
# builds don't have to re-install them. The image layer is cached
# publicly in your project's package repository, so it is vital that
# no secrets are present in the container state or logs.
install: |
case "${{ matrix.distro }}" in
ubuntu*|jessie|stretch|buster|bullseye)
apt-get update -q -y
apt-get install -q -y git build-essential cmake gcc g++
cmake --version
;;
fedora*)
dnf -y update
dnf -y install git which make cmake gcc-c++ gcc
cmake --version
;;
alpine*)
apk update
apk add git cmake gcc g++
cmake --version
;;
esac
# Produce a binary artifact and place it in the mounted volume
run: |
git clone https://github.com/drowe67/codec2.git
cd codec2
git checkout main
mkdir build
cd build
cmake ../
make
mv ./src/libcodec2.so.1.2 /artifacts/${artifact_name}
- name: Show recursive PWD/artifacts
# Items placed in /artifacts in the container will be in
# ${PWD}/artifacts on the host.
run: ls -al "${PWD}/artifacts"
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.libcodec2_os_name }}
#path: $GITHUB_WORKSPACE/codec2/artifacts/*
path: artifacts/*
build_i686_x64_release:
needs: [BUILD_AMD64, BUILD_ARM]
name: Build FreeDATA packages
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, macos-latest, windows-latest]
include:
- os: ubuntu-20.04
zip_name: ubuntu_modem
generator: Unix Makefiles
daemon_binary_name: freedata-daemon
modem_binary_name: freedata-modem
electron_parameters: "-p always"
- os: macos-latest
zip_name: macos_modem
generator: Unix Makefiles
daemon_binary_name: freedata-daemon
modem_binary_name: freedata-modem
electron_parameters: "-p always"
- os: windows-latest
zip_name: windows_modem
generator: Visual Studio 16 2019
daemon_binary_name: freedata-daemon.exe
modem_binary_name: freedata-modem.exe
electron_parameters: "-p always --x64 --ia32"
steps:
- name: Checkout code for ${{ matrix.platform.name }}
uses: actions/checkout@v4
with:
repository: DJ2LS/FreeDATA
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v4
with:
node-version: 18.17
- name: Create modem/dist
working-directory: modem
run: |
mkdir -p dist
- name: Create modem/dist/modem
working-directory: modem
run: |
mkdir -p dist/modem
##- name: Download libcodec2 artifact Modem DIST
## uses: actions/download-artifact@v4
## with:
## path: modem/dist/codec2
- name: create modem/lib/codec2
working-directory: modem/lib/
run: |
mkdir codec2
- name: Download libcodec2 artifact Modem LIB
uses: actions/download-artifact@v4
with:
path: modem/lib/codec2
- name: Install Linux dependencies
# if: matrix.os == 'ubuntu-20.04'
if: ${{startsWith(matrix.os, 'ubuntu')}}
run: |
sudo apt install -y portaudio19-dev libhamlib-dev libhamlib-utils build-essential cmake python3-libhamlib2 patchelf
- name: Install MacOS pyAudio
if: ${{startsWith(matrix.os, 'macos')}}
run: |
brew install portaudio
python -m pip install --upgrade pip
pip3 install pyaudio
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Add MacOS certs
if: ${{startsWith(matrix.os, 'macos')}}
run: chmod +x add-osx-cert.sh && ./add-osx-cert.sh
env:
CERTIFICATE_OSX_APPLICATION: ${{ secrets.CERTIFICATE_OSX_APPLICATION }}
CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
- name: Build binaries macOS
if: ${{startsWith(matrix.os, 'macos')}}
working-directory: modem
run: |
# now build modem binaries
pyinstaller -y freedata.spec
# and to some final cleanup
# cp -r -f dist/modem/* dist/
# rm -r dist/modem
- name: Build binaries Linux and Windows
if: ${{!startsWith(matrix.os, 'macos')}}
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.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: modem
run: |
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: modem
# These are powershell aliases, not UNIX commands.
run: |
cp -r -Force daemon.dist/* dist/modem
cp -r -Force main.dist/* dist/modem
- name: Rename modem binaries
# we don't need renaming for pyinstaller builds as output name is defined
if: ${{!startsWith(matrix.os, 'macos')}}
working-directory: modem
run: |
mv dist/modem/daemon* dist/modem/${{ matrix.daemon_binary_name }}
mv dist/modem/main* dist/modem/${{ matrix.modem_binary_name }}
- uses: actions/download-artifact@v4
with:
path: modem/dist/modem
- name: LIST ALL FILES
run: ls -R
- name: Download Portaudio binaries Linux macOS
if: ${{!startsWith(matrix.os, 'windows')}}
working-directory: modem
run: |
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: modem
run: |
if(Test-Path -Path "dist/modem/_sounddevice_data"){
echo "sounddevice folder already exists"
} else {
git clone https://github.com/spatialaudio/portaudio-binaries dist/modem/_sounddevice_data/portaudio-binaries
}
- name: LIST ALL FILES
run: ls -R
- name: cleanup on macos before code signing
if: ${{startsWith(matrix.os, 'macos')}}
run: |
ls -l
# find . -type d -name .git -exec rm -r {} \;
find . -type d -o -name ".git" -delete
- 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 }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
working-directory: gui
run: |
npm i
npm run release
- name: Compress Modem
uses: thedoctor0/zip-release@master
with:
type: 'zip'
filename: '${{ matrix.zip_name }}.zip'
# directory: ./modem/dist/modem
directory: ./modem/dist/modem
path: .
# exclusions: '*.git* /*node_modules/* .editorconfig'
- name: Release Modem
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/v')
with:
draft: true
files: ./modem/dist/modem/${{ matrix.zip_name }}.zip
#files: ./modem/dist/${{ matrix.zip_name }}.zip
- name: LIST ALL FILES
run: ls -R
#- name: Upload Modem artifacts
# uses: actions/upload-artifact@v4
# if: ${{!startsWith(github.ref, 'refs/tags/v')}}
# with:
# name: ${{ 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@v4
# if: ${{!startsWith(github.ref, 'refs/tags/v')}}
# with:
# name: app_bundle_${{ matrix.os }}.zip
# # path: ./modem/dist/modem/${{ matrix.zip_name }}.zip
# path: ./gui/dist/*

133
.github/workflows/build_server.yml vendored Normal file
View file

@ -0,0 +1,133 @@
name: build_server
on: [push]
jobs:
build_i686_x64_release:
name: Build FreeDATA packages
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, macos-latest, windows-latest]
include:
- os: ubuntu-20.04
zip_name: freedata-server_ubuntu
generator: Unix Makefiles
modem_binary_name: freedata-server
- os: macos-latest
zip_name: freedata-server_macos
generator: Unix Makefiles
modem_binary_name: freedata-server
- os: windows-latest
zip_name: freedata-server_windows
generator: Visual Studio 16 2019
modem_binary_name: freedata-server.exe
steps:
- name: Checkout code for ${{ matrix.platform.name }}
uses: actions/checkout@v4
with:
repository: DJ2LS/FreeDATA
- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Create modem/dist
working-directory: modem
run: |
mkdir -p dist
- name: Create modem/dist/modem
working-directory: modem
run: |
mkdir -p dist/modem
- name: Install Linux dependencies
# if: matrix.os == 'ubuntu-20.04'
if: ${{startsWith(matrix.os, 'ubuntu')}}
run: |
sudo apt install -y portaudio19-dev libhamlib-dev libhamlib-utils build-essential cmake python3-libhamlib2 patchelf
- name: Install MacOS pyAudio
if: ${{startsWith(matrix.os, 'macos')}}
run: |
brew install portaudio
python -m pip install --upgrade pip
pip3 install pyaudio
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Add MacOS certs
if: ${{startsWith(matrix.os, 'macos')}}
run: chmod +x add-osx-cert.sh && ./add-osx-cert.sh
env:
CERTIFICATE_OSX_APPLICATION: ${{ secrets.CERTIFICATE_OSX_APPLICATION }}
CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
- name: Build binaries
working-directory: modem
run: |
python3 -m nuitka --remove-output --assume-yes-for-downloads --follow-imports --include-data-dir=lib=lib --include-data-files=lib/codec2/*=lib/codec2/ --include-data-files=config.ini.example=config.ini --standalone server.py --output-filename=freedata-server
#- name: Download Portaudio binaries Linux macOS
# if: ${{!startsWith(matrix.os, 'windows')}}
# working-directory: modem
# run: |
# if ! test -d "server.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: modem
# run: |
# if(Test-Path -Path "server.dist/modem/_sounddevice_data"){
# echo "sounddevice folder already exists"
# } else {
# git clone https://github.com/spatialaudio/portaudio-binaries dist/modem/_sounddevice_data/portaudio-binaries
# }
- name: LIST ALL FILES
run: ls -R
- name: cleanup on macos before code signing
if: ${{startsWith(matrix.os, 'macos')}}
run: |
ls -l
# find . -type d -name .git -exec rm -r {} \;
find . -type d -o -name ".git" -delete
- name: Compress Modem
uses: thedoctor0/zip-release@master
with:
type: 'zip'
filename: '${{ matrix.zip_name }}.zip'
directory: ./modem/server.dist
path: .
# exclusions: '*.git* /*node_modules/* .editorconfig'
- name: LIST ALL FILES
run: ls -R
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: '${{ matrix.zip_name }}'
path: ./modem/server.dist/${{ matrix.zip_name }}.zip
- name: Release Modem
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/v')
with:
draft: true
files: ./modem/server.dist/${{ matrix.zip_name }}.zip
- name: LIST ALL FILES
run: ls -R

38
.github/workflows/gui_tests.yml vendored Normal file
View file

@ -0,0 +1,38 @@
name: GUI tests
on: [push]
jobs:
build:
# The CMake configure and build commands are platform-agnostic and should work equally
# well on Windows or Mac. You can convert this to a matrix build if you need
# cross-platform coverage.
# See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix
runs-on: ubuntu-latest
strategy:
# By default, GitHub will maximize the number of jobs run in parallel
# depending on the available runners on GitHub-hosted virtual machines.
# max-parallel: 8
fail-fast: false
matrix:
include:
- node-version: "16"
- node-version: "18"
- node-version: "20"
steps:
- uses: actions/checkout@v4
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
working-directory: gui
run: |
npm i
- name: GUI Test
working-directory: gui
run: |
npm run test

View file

@ -1,4 +1,4 @@
name: CTest
name: Modem tests
on: [push]
@ -16,12 +16,12 @@ jobs:
fail-fast: false
matrix:
include:
- python-version: "3.7"
#- python-version: "3.7" EOL
- python-version: "3.8"
- python-version: "3.9"
- python-version: "3.10"
- python-version: "3.11"
#- python-version: "3.12-dev"
#- python-version: "3.12" NOT YET SUPPORTED BY NUITKA!
steps:
- uses: actions/checkout@v4
@ -32,25 +32,18 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install packages
- name: Install system packages
shell: bash
run: |
sudo apt-get update
sudo apt-get install octave octave-common octave-signal sox portaudio19-dev python3-pyaudio
pip3 install psutil crcengine ujson pyserial numpy structlog sounddevice pyaudio requests websocket-client
pip3 install pytest pytest-rerunfailures
sudo apt-get update || true
sudo apt-get install octave octave-common octave-signal sox portaudio19-dev
- name: Build codec2
- name: Install python packages
shell: bash
run: |
git clone https://github.com/drowe67/codec2.git
cd codec2
mkdir -p build_linux && cd build_linux && cmake .. && make
pip3 install -r requirements.txt
- name: run ctests
- name: run config tests
shell: bash
working-directory: ${{github.workspace}}
run: |
mkdir build && cd build
cmake -DCODEC2_BUILD_DIR=$GITHUB_WORKSPACE/codec2/build_linux ..
ctest --output-on-failure
python -m unittest discover tests

5
.gitignore vendored
View file

@ -29,4 +29,7 @@ coverage.xml
#Ignore gui build items
/gui/dist
/gui/release
/gui/dist-electron
/gui/dist-electron
#Ignore GUI config
/gui/config/config.json

View file

@ -1,218 +0,0 @@
cmake_minimum_required(VERSION 3.0)
project (FreeDATA)
include(CTest)
enable_testing()
# Find codec2
if(CODEC2_BUILD_DIR)
find_package(codec2 REQUIRED
PATHS ${CODEC2_BUILD_DIR}
NO_DEFAULT_PATH
CONFIGS codec2.cmake
)
if(codec2_FOUND)
message(STATUS "Codec2 library found in build tree.")
endif()
else()
find_package(codec2 REQUIRED)
endif()
# test variables
set(FRAMESPERBURST 3)
set(BURSTS 1)
set(TESTFRAMES 3)
add_test(NAME audio_buffer
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
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=../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 modem_state_machine
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../modem;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_modem_states.py")
set_tests_properties(modem_state_machine PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
add_test(NAME modem_irs_iss
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../modem;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
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=../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=../modem;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_datac13.py")
set_tests_properties(datac13_frames PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
# 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=../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=../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=../modem;
export BURSTS=${BURSTS};
export FRAMESPERBURST=${FRAMESPERBURST};
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_highsnr_stdio_P_P_multi.py")
set_tests_properties(py_highsnr_stdio_P_P_multi PROPERTIES PASS_REGULAR_EXPRESSION "DATAC13: ${BURSTS}/${FRAMESPERBURST} DATAC1: ${BURSTS}/${FRAMESPERBURST} DATAC3: ${BURSTS}/${FRAMESPERBURST}")
add_test(NAME py_highsnr_stdio_P_P_datacx
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../modem;
export BURSTS=${BURSTS};
export FRAMESPERBURST=${FRAMESPERBURST};
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_highsnr_stdio_P_P_datacx.py")
set_tests_properties(py_highsnr_stdio_P_P_datacx PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
add_test(NAME py_highsnr_stdio_P_C_datacx
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../modem;
export BURSTS=${BURSTS};
export FRAMESPERBURST=${FRAMESPERBURST};
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_highsnr_stdio_P_C_datacx.py")
set_tests_properties(py_highsnr_stdio_P_C_datacx PROPERTIES PASS_REGULAR_EXPRESSION "HELLO WORLD")
add_test(NAME py_highsnr_stdio_C_P_datacx
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../modem;
export BURSTS=${BURSTS};
export FRAMESPERBURST=${FRAMESPERBURST};
export TESTFRAMES=${TESTFRAMES};
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_highsnr_stdio_C_P_datacx.py")
set_tests_properties(py_highsnr_stdio_C_P_datacx PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
add_test(NAME highsnr_stdio_P_C_single
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 util_tx.py --mode datac13 --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} |
sox -t .s16 -r 48000 -c 1 - -t .s16 -r 8000 -c 1 - |
freedv_data_raw_rx datac13 - - --framesperburst ${FRAMESPERBURST} | hexdump -C")
set_tests_properties(highsnr_stdio_P_C_single PROPERTIES PASS_REGULAR_EXPRESSION "HELLO WORLD")
add_test(NAME highsnr_stdio_C_P_single
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
freedv_data_raw_tx datac13 --testframes ${TESTFRAMES} --bursts ${BURSTS} --framesperburst ${FRAMESPERBURST} /dev/zero - |
sox -t .s16 -r 8000 -c 1 - -t .s16 -r 48000 -c 1 - |
python3 util_rx.py --mode datac13 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS}")
set_tests_properties(highsnr_stdio_C_P_single PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
add_test(NAME highsnr_stdio_P_P_single
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 util_tx.py --mode datac13 --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} |
python3 util_rx.py --debug --mode datac13 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS}")
set_tests_properties(highsnr_stdio_P_P_single PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
add_test(NAME highsnr_stdio_P_P_multi
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 util_multimode_tx.py --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} |
python3 util_multimode_rx.py --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} --timeout 60")
set_tests_properties(highsnr_stdio_P_P_multi PROPERTIES PASS_REGULAR_EXPRESSION "DATAC13: ${BURSTS}/${FRAMESPERBURST} DATAC1: ${BURSTS}/${FRAMESPERBURST} DATAC3: ${BURSTS}/${FRAMESPERBURST}")
# These tests can't run on GitHub actions as we don't have a virtual sound card
if(NOT DEFINED ENV{GITHUB_RUN_ID})
# uses aplay/arecord then pipe to Python
add_test(NAME highsnr_virtual1_P_P_single_alsa
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
./test_virtual1.sh")
set_tests_properties(highsnr_virtual1_P_P_single_alsa PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 5 RECEIVED FRAMES: 10 RX_ERRORS: 0")
# let Python do audio I/O
add_test(NAME highsnr_virtual2_P_P_single
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
./test_virtual2.sh")
set_tests_properties(highsnr_virtual2_P_P_single PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0")
# Multimode test with Python I/O
add_test(NAME highsnr_virtual3_P_P_multi
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
./test_virtual_mm.sh")
set_tests_properties(highsnr_virtual3_P_P_multi PROPERTIES PASS_REGULAR_EXPRESSION "DATAC13: 2/4 DATAC1: 2/4 DATAC3: 2/4")
# let Python do audio I/O via pyaudio callback mode
add_test(NAME highsnr_virtual4_P_P_single_callback
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
./test_virtual3a.sh")
set_tests_properties(highsnr_virtual4_P_P_single_callback PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0")
# let Python do audio I/O via pyaudio callback mode with code outside of callback
add_test(NAME highsnr_virtual4_P_P_single_callback_outside
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
./test_virtual3b.sh")
set_tests_properties(highsnr_virtual4_P_P_single_callback_outside PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: 3 RECEIVED FRAMES: 6 RX_ERRORS: 0")
# let Python do audio I/O via pyaudio callback mode with code outside of callback
add_test(NAME highsnr_virtual5_P_P_multi_callback
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
./test_virtual4a.sh")
set_tests_properties(highsnr_virtual5_P_P_multi_callback PROPERTIES PASS_REGULAR_EXPRESSION "DATAC13: 2/4 DATAC1: 2/4 DATAC3: 2/4")
# let Python do audio I/O via pyaudio callback mode with code outside of callback
add_test(NAME highsnr_virtual5_P_P_multi_callback_outside
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
./test_virtual4b.sh")
set_tests_properties(highsnr_virtual5_P_P_multi_callback_outside PROPERTIES PASS_REGULAR_EXPRESSION "DATAC13: 2/4 DATAC1: 2/4 DATAC3: 2/4")
endif()

View file

@ -0,0 +1,141 @@
# FreeDATA - Protocols
## ARQ Sessions
An ARQ Session represents a reliable data transmission session from a sending station (A) to a receiving station (B). It uses automatic repeat request on top of different codec2 modes according to the transmission channel conditions.
So lets say A wants to send some data to B. A typical scenario would be like this:
```
ISS->(1)IRS:<datac13> OPEN_REQ(session id, origin, dest)
IRS->(1)ISS:<datac13> OPEN_ACK (session id, proto version, speed level, frames, snr)
ISS->(1)IRS:<datac13> INFO(id, total_bytes, total_crc)
IRS->(1)ISS:<datac13> INFO_ACK(id, total_crc)
ISS->(1)IRS:BURST (ID, offset, payload),(ID, offset, payload),(ID, offset, payload)
IRS->(1)ISS:BURST_ACK (ID, next_offset, speed level, frames, snr)
ISS-->(1)IRS:Lost BURST (total or part)
IRS->(1)ISS:BURST_NACK (ID, next_offset, speed level, frames, snr)
ISS->(1)IRS:BURST (ID, offset, payload),(ID, offset, payload),(ID, offset, payload)
IRS->(1)ISS:DATA ACK NACK (ID, next_offset, speed level, frames, snr)
```
### Frame details
#### SESSION_OPEN_REQ
ISS sends this first
DATAC13 Mode (12 bytes)
|field|bytes|
|-|-|
|session id|1|
|origin|6|
|destination_crc|3|
#### SESSION_OPEN_ACK
Sent by the IRS in response to a SESSION_OPEN_REQ
DATAC13 Mode (12 bytes)
|field|bytes|
|-|-|
|session id|1|
|origin|6|
|destination_crc|3|
|protocol version|1|
|snr|1|
#### SESSION_INFO
ISS sends this in response to a SESSION_OPEN_ACK
DATAC13 Mode (12 bytes)
|field|bytes|
|-|-|
|session id|1|
|total bytes|4|
|total crc|4|
|snr|1|
#### SESSION_INFO_ACK
IRS sends this in response to a SESSION_INFO
DATAC13 Mode (12 bytes)
|field|bytes|
|-|-|
|session id|1|
|total crc|4|
|snr|1|
|speed level|1|
|frames per burst|1|
#### Data Burst
ISS sends this to send data to IRS
Mode according to handshake speed level
Frames per burst according to handshake
##### Modulation
Each burst is composed of frames_per_burst frames:
|preamble|f1|f2|f3|...|postamble|
##### Each data frame
|field|bytes|
|-|-|
|session id|1|
|offset|4|
|payload|(the remaining payload length)|
#### DATA_BURST_ACK
Sent by the IRS following successful decoding of burst.
|field|bytes|
|-|-|
|session id|1|
|next offset|4|
|next speed level|1|
|next frames per burst|1|
|snr|1|
#### DATA_BURST_NACK
Sent by the IRS following unsuccessful decoding of burst or timeout.
|field|bytes|
|-|-|
|session id|1|
|next offset|4|
|next speed level|1|
|next frames per burst|1|
|snr|1|
#### DATA ACK NACK
Sent by the IRS after receiving data with a state information.
| field |bytes|
|------------|-|
| session id |1|
| state |1|
| snr |1|

10
gui/config/example.json Normal file
View file

@ -0,0 +1,10 @@
{
"local": {
"host": "127.0.0.1",
"port": "5000",
"spectrum": "waterfall",
"wf_theme": 2,
"update_channel": "alpha",
"enable_sys_notification": false
}
}

View file

@ -19,13 +19,13 @@
"files": [
"dist",
"dist-electron",
"../modem/dist/modem/",
"../modem/server.dist/",
],
"extraResources": [
{
"from": "../modem/dist/modem/",
"from": "../modem/server.dist/",
"to": "modem",
"filter": [
"**/*",

View file

@ -20,6 +20,7 @@ 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;
process.env.FDUpdateAvail = "0";
// Disable GPU Acceleration for Windows 7
if (release().startsWith("6.1")) app.disableHardwareAcceleration();
@ -55,8 +56,7 @@ async function createWindow() {
autoHideMenuBar: true,
webPreferences: {
preload,
backgroundThrottle: false,
backgroundThrottling: 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
@ -100,20 +100,20 @@ async function createWindow() {
app.whenReady().then(() => {
createWindow();
console.log(platform())
console.log(platform());
//Generate daemon binary path
var daemonPath = "";
switch (platform().toLowerCase()) {
case "darwin":
daemonPath = join(process.resourcesPath, "modem", "freedata-daemon");
daemonPath = join(process.resourcesPath, "modem", "freedata-server");
case "linux":
daemonPath = join(process.resourcesPath, "modem", "freedata-daemon");
daemonPath = join(process.resourcesPath, "modem", "freedata-server");
break;
case "win32":
daemonPath = join(process.resourcesPath, "modem", "freedata-daemon.exe");
break;
daemonPath = join(process.resourcesPath, "modem", "freedata-server.exe");
break;
case "win64":
daemonPath = join(process.resourcesPath, "modem", "freedata-daemon.exe");
daemonPath = join(process.resourcesPath, "modem", "freedata-server.exe");
break;
default:
console.log("Unhandled OS Platform: ", platform());
@ -122,24 +122,22 @@ console.log(platform())
//Start daemon binary if it exists
if (existsSync(daemonPath)) {
console.log("Starting freedata-daemon binary");
console.log("Starting freedata-server binary");
console.log("daemonPath:", daemonPath);
console.log("CWD:", join(daemonPath, ".."));
/*
var daemonProcess = spawn("freedata-daemon", [], {
/*
var daemonProcess = spawn("freedata-server", [], {
cwd: join(process.env.DIST, "modem"),
shell: true
});
*/
/*
/*
daemonProcess = spawn(daemonPath, [], {
shell: true
});
console.log(daemonProcess)
*/
daemonProcess = spawn(daemonPath, [], {
});
daemonProcess = spawn(daemonPath, [], {});
// return process messages
daemonProcess.on("error", (err) => {
@ -154,7 +152,7 @@ daemonProcess = spawn(daemonPath, [], {
});
daemonProcess.stderr.on("data", (data) => {
// daemonProcessLog.info(`${data}`);
console.log(data)
console.log(data);
});
daemonProcess.on("close", (code) => {
// daemonProcessLog.warn(`daemonProcess exited with code ${code}`);
@ -170,7 +168,7 @@ daemonProcess = spawn(daemonPath, [], {
app.on("window-all-closed", () => {
win = null;
if (process.platform !== "darwin") app.quit(close_sub_processes());
if (process.platform !== "darwin") app.quit();
});
app.on("second-instance", () => {
@ -215,6 +213,7 @@ ipcMain.on("request-restart-and-install-update", (event, data) => {
// LISTENER FOR UPDATER EVENTS
autoUpdater.on("update-available", (info) => {
process.env.FDUpdateAvail = "1";
console.log("update available");
let arg = {
@ -234,6 +233,7 @@ autoUpdater.on("update-not-available", (info) => {
});
autoUpdater.on("update-downloaded", (info) => {
process.env.FDUpdateAvail = "1";
console.log("update downloaded");
let arg = {
status: "update-downloaded",
@ -288,19 +288,19 @@ function close_sub_processes() {
console.log("closing modem and daemon");
try {
if (platform() == "win32" || platform() == "win64") {
if (platform() == "win32") {
spawn("Taskkill", ["/IM", "freedata-modem.exe", "/F"]);
spawn("Taskkill", ["/IM", "freedata-daemon.exe", "/F"]);
spawn("Taskkill", ["/IM", "freedata-server.exe", "/F"]);
}
if (platform() == "linux") {
spawn("pkill", ["-9", "freedata-modem"]);
spawn("pkill", ["-9", "freedata-daemon"]);
spawn("pkill", ["-9", "freedata-server"]);
}
if (platform() == "darwin") {
spawn("pkill", ["-9", "freedata-modem"]);
spawn("pkill", ["-9", "freedata-daemon"]);
spawn("pkill", ["-9", "freedata-server"]);
}
} catch (e) {
console.log(e);

View file

@ -113,17 +113,6 @@ window.onmessage = (ev) => {
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") {

View file

@ -7,6 +7,7 @@
"scripts": {
"start": "vite",
"dev": "vite",
"test": "vitest --run",
"check": "vue-tsc --noEmit",
"build": "vue-tsc --noEmit && vite build && electron-builder -p never",
"release": "vue-tsc --noEmit && vite build && electron-builder -p onTag",
@ -31,55 +32,59 @@
},
"homepage": "https://freedata.app",
"dependencies": {
"@electron/asar": "^3.2.7",
"@electron/notarize": "^2.1.0",
"@electron/universal": "^2.0.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",
"bootswatch": "^5.3.1",
"browser-image-compression": "^2.0.2",
"chart.js": "^4.3.3",
"chartjs-plugin-annotation": "^3.0.1",
"electron-log": "^5.0.0",
"electron-updater": "^6.1.6",
"emoji-picker-element": "^1.18.3",
"emoji-picker-element-data": "^1.4.0",
"file-saver": "^2.0.5",
"mime": "^3.0.0",
"pinia": "^2.1.6",
"pouchdb": "^8.0.1",
"pouchdb-browser": "^8.0.1",
"pouchdb-find": "^8.0.1",
"pouchdb-upsert": "^2.2.0",
"qth-locator": "^2.1.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"
"@electron/asar": "3.2.7",
"@electron/notarize": "2.2.0",
"@electron/universal": "2.0.0",
"@popperjs/core": "2.11.8",
"@vueuse/electron": "10.7.1",
"blob-util": "2.0.2",
"bootstrap": "5.3.2",
"bootstrap-icons": "1.11.2",
"bootswatch": "5.3.2",
"browser-image-compression": "2.0.2",
"chart.js": "4.4.1",
"chartjs-plugin-annotation": "3.0.1",
"electron-log": "5.0.3",
"electron-updater": "6.1.7",
"emoji-picker-element": "1.20.1",
"emoji-picker-element-data": "1.6.0",
"file-saver": "2.0.5",
"gridstack": "10.0.1",
"mime": "4.0.1",
"nconf": "^0.12.1",
"pinia": "2.1.7",
"pouchdb": "8.0.1",
"pouchdb-browser": "8.0.1",
"pouchdb-find": "8.0.1",
"pouchdb-upsert": "2.2.0",
"qth-locator": "2.1.0",
"sass": "1.66.1",
"socket.io": "4.7.2",
"uuid": "9.0.1",
"vue": "3.3.12",
"vue-chartjs": "5.3.0",
"vuemoji-picker": "0.2.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@vitejs/plugin-vue": "^4.4.0",
"electron": "^27.0.0",
"electron-builder": "^24.6.3",
"eslint": "^8.50.0",
"eslint-config-prettier": "^9.0.0",
"eslint-config-standard-with-typescript": "^40.0.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": "^5.0.2",
"vite-plugin-electron": "^0.15.4",
"vite-plugin-electron-renderer": "^0.14.5",
"vue": "^3.3.4",
"vue-tsc": "^1.4.2"
"@types/nconf": "^0.10.6",
"@typescript-eslint/eslint-plugin": "6.17.0",
"@vitejs/plugin-vue": "4.5.2",
"electron": "28.1.3",
"electron-builder": "24.9.1",
"eslint": "8.56.0",
"eslint-config-prettier": "9.1.0",
"eslint-config-standard-with-typescript": "43.0.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-n": "16.1.0",
"eslint-plugin-prettier": "5.0.1",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-vue": "9.20.1",
"typescript": "5.3.3",
"vite": "5.0.10",
"vite-plugin-electron": "0.28.0",
"vite-plugin-electron-renderer": "0.14.5",
"vitest": "1.0.2",
"vue": "3.3.12",
"vue-tsc": "1.8.27"
}
}

View file

@ -274,14 +274,16 @@ Spectrum.prototype.updateSpectrumRatio = function () {
};
Spectrum.prototype.resize = function () {
var width = this.canvas.clientWidth;
var height = this.canvas.clientHeight;
var width = this.parent.clientWidth;
var height =this.parent.clientHeight;
// little helper for setting height of clientHeight is not working as expected
if (height == 0){
var height = 250
}
if (width == 0){
width=500;
}
if (this.canvas.width != width || this.canvas.height != height) {
this.canvas.width = width;
@ -445,8 +447,8 @@ Spectrum.prototype.onKeypress = function (e) {
export function Spectrum(id, options) {
console.log("waterfall init....")
console.log(document.getElementById(id))
// console.log("waterfall init....")
//console.log(document.getElementById(id))
// Handle options
this.centerHz = options && options.centerHz ? options.centerHz : 1500;
@ -473,7 +475,7 @@ export function Spectrum(id, options) {
// Create main canvas and adjust dimensions to match actual
this.canvas = document.getElementById(id);
this.parent = this.canvas.parentElement;
this.canvas.height = this.canvas.clientHeight;
this.canvas.width = this.canvas.clientWidth;

View file

@ -33,7 +33,7 @@ import chat_new_message from "./chat_new_message.vue";
<div
class="container overflow-auto"
id="message-container"
style="height: calc(100% - 200px)"
style="height: calc(100% - 225px)"
>
<chat_messages />
</div>

View file

@ -57,6 +57,7 @@
>
<div
class="progress rounded-0 rounded-bottom"
hidden
:style="{ height: '10px' }"
v-bind:class="{
'bg-danger': message.status == 'failed',

View file

@ -11,7 +11,7 @@ const state = useStateStore(pinia);
import { useChatStore } from "../store/chatStore.js";
const chat = useChatStore(pinia);
import { getRxBuffer } from "../js/sock.js";
import { startChatWithNewStation } from "../js/chatHandler";
import {
Chart as ChartJS,
@ -88,21 +88,6 @@ var beaconHistogramOptions = {
},
};
//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: [
@ -121,8 +106,10 @@ const beaconHistogramData = computed(() => ({
function newChat() {
let callsign = this.newChatCall.value;
callsign = callsign.toUpperCase();
chat.callsign_list.add(callsign);
callsign = callsign.toUpperCase().trim();
if (callsign === "") return;
startChatWithNewStation(callsign);
//updateAllChat(false);
this.newChatCall.value = "";
}

View file

@ -1,15 +1,10 @@
<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);
@ -55,7 +50,7 @@ const chatModuleMessage=ref(null);
function transmitNewMessage(){
chat.inputText = chat.inputText.trim();
if (chat.inputText.length==0)
if (chat.inputText.length==0 && chat.inputFileName == "-")
return;
if (chat.selectedCallsign.startsWith("BC-")) {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,15 @@
<script setup lang="ts">
const props = defineProps(["btnText", "btnID"]);
function emitClick() {
window.dispatchEvent(new CustomEvent("add-widget", { detail: props.btnID }));
}
</script>
<template>
<button
class="btn btn-small btn-outline-secondary mb-1"
v-on:click="emitClick"
>
{{ btnText }}
</button>
</template>

View file

@ -0,0 +1,17 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../../store/index";
setActivePinia(pinia);
import { sendModemCQ } from "../../js/api.js";
</script>
<template>
<div class="fill h-100" style="width: calc(100% - 24px)">
<a
class="btn btn-sm btn-secondary d-flex justify-content-center align-items-center object-fill border rounded w-100 h-100"
@click="sendModemCQ"
title="Send a CQ call!"
>CQ</a
>
</div>
</template>

View file

@ -0,0 +1,140 @@
<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>
<div class="card w-100 h-100">
<div class="card-header p-0 mb-1">
<i class="bi bi-volume-up" style="font-size: 1rem"></i>&nbsp;
<strong>Audio</strong>
</div>
<div class="card-body pt-0 pb-0">
<div class="container-wide">
<div class="row">
<div class="col-lg-6">
<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-lg-6">
<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,85 @@
<script setup lang="ts">
import { ref } from "vue";
import { setActivePinia } from "pinia";
import pinia from "../../store/index";
setActivePinia(pinia);
import { useStateStore } from "../../store/stateStore.js";
const state = useStateStore(pinia);
import { sendModemCQ, sendModemPing, setModemBeacon } from "../../js/api.js";
function transmitPing() {
sendModemPing(dxcallPing.value.toUpperCase());
}
function startStopBeacon() {
if (state.beacon_state === true) {
setModemBeacon(false);
} else {
setModemBeacon(true);
}
}
var dxcallPing = ref("");
</script>
<template>
<div class="card h-100">
<div class="card-header p-0">
<i class="bi bi-broadcast" style="font-size: 1.2rem"></i>&nbsp;
<strong>Broadcasts</strong>
</div>
<div class="card-body overflow-auto p-0">
<div class="input-group input-group-sm mb-0">
<input
type="text"
class="form-control"
style="max-width: 6rem; min-width: 3rem; text-transform: uppercase"
placeholder="DXcall"
pattern="[A-Z]*"
maxlength="11"
aria-label="Input group"
aria-describedby="btnGroupAddon"
v-model="dxcallPing"
/>
<button
class="btn btn-sm btn-outline-secondary"
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="sendModemCQ()"
>
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."
>
Toggle beacon
</button>
</div>
<!-- end of row-->
</div>
</div>
</template>

View file

@ -0,0 +1,97 @@
<script setup lang="ts">
import { ref } from "vue";
import { setActivePinia } from "pinia";
import pinia from "../../store/index";
setActivePinia(pinia);
import { useStateStore } from "../../store/stateStore.js";
const state = useStateStore(pinia);
import { sendModemCQ, sendModemPing, setModemBeacon } from "../../js/api.js";
function transmitPing() {
sendModemPing(dxcallPing.value.toUpperCase());
}
function startStopBeacon() {
if (state.beacon_state === true) {
setModemBeacon(false);
} else {
setModemBeacon(true);
}
}
var dxcallPing = ref("");
</script>
<template>
<div class="card h-100">
<div class="card-header p-0">
<i class="bi bi-broadcast" style="font-size: 1.2rem"></i>&nbsp;
<strong>Broadcasts</strong>
</div>
<div class="card-body overflow-auto p-0">
<div class="container text-center">
<div class="row mb-2 mt-2">
<div class="col-sm-8">
<div class="input-group w-100">
<div class="form-floating">
<input
type="text"
class="form-control"
style="text-transform: uppercase"
id="floatingInput"
placeholder="dx-callsign"
v-model="dxcallPing"
maxlength="11"
pattern="[A-Z]*"
/>
<label for="floatingInput">DX-Callsign</label>
</div>
<button
class="btn btn-sm btn-outline-secondary"
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()"
>
<strong>Ping</strong>
</button>
</div>
</div>
<div class="col">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="flexSwitchBeacon"
v-model="state.beacon_state"
@click="startStopBeacon()"
/>
<label class="form-check-label" for="flexSwitchBeacon"
>Beacon</label
>
</div>
</div>
</div>
<div class="row">
<div class="col">
<button
class="btn btn-sm btn-outline-secondary w-100"
id="sendCQ"
type="button"
title="Send a CQ to the world"
@click="sendModemCQ()"
>
<h3>CQ CQ CQ</h3>
</button>
</div>
</div>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,90 @@
<script setup lang="ts">
// @ts-nocheck
const { distance } = require("qth-locator");
import { setActivePinia } from "pinia";
import pinia from "../../store/index";
setActivePinia(pinia);
import { settingsStore as settings } from "../../store/settingsStore.js";
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: "2-digit",
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.remote.STATION.mygrid, dxGrid));
} catch (e) {
//
}
}
</script>
<template>
<div class="card h-100">
<!--325px-->
<div class="card-header p-0">
<i class="bi bi-list-columns-reverse" style="font-size: 1.2rem"></i>&nbsp;
<strong>Heard stations</strong>
</div>
<div class="card-body overflow-auto p-0">
<div class="table-responsive">
<!-- START OF TABLE FOR HEARD STATIONS -->
<table class="table table-sm table-striped" id="tblHeardStationList">
<thead>
<tr>
<th scope="col" id="thTime">Time</th>
<th scope="col" id="thFreq">Freq</th>
<th scope="col" id="thDxcall">DXCall</th>
<th scope="col" id="thDxgrid">Grid</th>
<th scope="col" id="thDist">Dist</th>
<th scope="col" id="thType">Type</th>
<th scope="col" id="thSnr">SNR</th>
<!--<th scope="col">Off</th>-->
</tr>
</thead>
<tbody id="gridHeardStations">
<!--https://vuejs.org/guide/essentials/list.html-->
<tr v-for="item in state.heard_stations" :key="item.origin">
<td>
{{ getDateTime(item.timestamp) }}
</td>
<td>{{ item.frequency / 1000 }} kHz</td>
<td>
{{ item.origin }}
</td>
<td>
{{ item.gridsquare }}
</td>
<td>{{ getMaidenheadDistance(item.gridsquare) }} km</td>
<td>
{{ item.activity_type }}
</td>
<td>
{{ item.snr }}
</td>
</tr>
</tbody>
</table>
</div>
<!-- END OF HEARD STATIONS TABLE -->
</div>
</div>
</template>

View file

@ -0,0 +1,74 @@
<script setup lang="ts">
// @ts-nocheck
const { distance } = require("qth-locator");
import { setActivePinia } from "pinia";
import pinia from "../../store/index";
setActivePinia(pinia);
import { settingsStore as settings } from "../../store/settingsStore.js";
import { useStateStore } from "../../store/stateStore.js";
const state = useStateStore(pinia);
function getDateTime(timestampRaw) {
var datetime = new Date(timestampRaw * 1000).toLocaleString(
navigator.language,
{
hourCycle: "h23",
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.remote.STATION.mygrid, dxGrid));
} catch (e) {
//
}
}
</script>
<template>
<div class="card h-100">
<div class="card-header p-0">
<i class="bi bi-list-columns-reverse" style="font-size: 1.2rem"></i>&nbsp;
<strong>Heard stations</strong>
</div>
<div class="card-body overflow-auto p-0">
<div class="table-responsive">
<!-- START OF TABLE FOR HEARD STATIONS -->
<table class="table table-sm table-striped" id="tblHeardStationList">
<thead>
<tr>
<th scope="col" id="thTime">Time</th>
<th scope="col" id="thDxcall">DXCall</th>
</tr>
</thead>
<tbody id="miniHeardStations">
<!--https://vuejs.org/guide/essentials/list.html-->
<tr v-for="item in state.heard_stations" :key="item.origin">
<td>
<span class="fs-6">{{ getDateTime(item.timestamp) }}</span>
</td>
<td>
<span>{{ item.origin }}</span>
</td>
<!--<td>{{ item.offset }}</td>-->
</tr>
</tbody>
</table>
</div>
<!-- END OF HEARD STATIONS TABLE -->
</div>
</div>
</template>

View file

@ -0,0 +1,94 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../../store/index";
import { setRadioParameters } from "../../js/api";
setActivePinia(pinia);
import { useStateStore } from "../../store/stateStore.js";
const state = useStateStore(pinia);
function set_radio_parameters() {
setRadioParameters(state.frequency, state.mode, state.rf_level);
}
</script>
<template>
<div class="card h-100">
<div class="card-header p-0">
<i class="bi bi-house-door" style="font-size: 1.2rem"></i>&nbsp;
<strong>Radio control</strong>
</div>
<div class="card-body overflow-auto p-0">
<div class="input-group input-group-sm bottom-0 m-0">
<div class="me-2">
<div class="input-group input-group-sm">
<span class="input-group-text">QRG</span>
<span class="input-group-text"
>{{ state.frequency / 1000 }} kHz</span
>
<button
class="btn btn-secondary dropdown-toggle"
v-bind:class="{
disabled: state.hamlib_status === 'disconnected',
}"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#offcanvasFrequency"
aria-controls="offcanvasExample"
></button>
</div>
</div>
<div class="me-2">
<div class="input-group input-group-sm">
<span class="input-group-text">Mode</span>
<select
class="form-control"
v-model="state.mode"
@click="set_radio_parameters()"
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 input-group-sm">
<span class="input-group-text">% Power</span>
<select
class="form-control"
v-model="state.rf_level"
@click="set_radio_parameters()"
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>
</div>
</div>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,363 @@
<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 { ref, computed, onMounted, nextTick } from "vue";
import { initWaterfall, setColormap } from "../../js/waterfallHandler.js";
import { setActivePinia } from "pinia";
import pinia from "../../store/index";
setActivePinia(pinia);
import { settingsStore as settings } from "../../store/settingsStore.js";
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";
const localSpectrumView = ref("waterfall");
function selectStatsControl(item) {
switch (item) {
case "wf":
localSpectrumView.value = "waterfall";
break;
case "scatter":
localSpectrumView.value = "scatter";
break;
case "chart":
localSpectrumView.value = "chart";
break;
default:
localSpectrumView.value = "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)",
},
],
}));
var localSpectrum;
//Define and generate a unique ID for canvas
const localSpectrumID = ref("");
localSpectrumID.value =
"gridwfid-" + (Math.random() + 1).toString(36).substring(7);
onMounted(() => {
// 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
localSpectrum = initWaterfall(localSpectrumID.value);
});
</script>
<template>
<div class="card h-100">
<div class="card-header p-1">
<div class="btn-group" role="group">
<div
class="list-group bg-body-tertiary list-group-horizontal"
id="list-tab"
role="tablist"
>
<a
class="py-0 list-group-item list-group-item-dark list-group-item-action"
data-bs-toggle="list"
role="tab"
aria-controls="list-waterfall"
v-bind:class="{
active: localSpectrumView == 'waterfall',
}"
@click="selectStatsControl('wf')"
><strong><i class="bi bi-water"></i></strong
></a>
<a
class="py-0 list-group-item list-group-item-dark list-group-item-action"
data-bs-toggle="list"
role="tab"
aria-controls="list-scatter"
v-bind:class="{
active: localSpectrumView == 'scatter',
}"
@click="selectStatsControl('scatter')"
><strong><i class="bi bi-border-outer"></i></strong
></a>
<a
class="py-0 list-group-item list-group-item-dark list-group-item-action"
data-bs-toggle="list"
role="tab"
aria-controls="list-chart"
v-bind:class="{ active: localSpectrumView == 'chart' }"
@click="selectStatsControl('chart')"
><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.channel_busy_slot[0] === true,
'btn-outline-secondary': state.channel_busy_slot[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.channel_busy_slot[1] === true,
'btn-outline-secondary': state.channel_busy_slot[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.channel_busy_slot[2] === true,
'btn-outline-secondary': state.channel_busy_slot[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.channel_busy_slot[3] === true,
'btn-outline-secondary': state.channel_busy_slot[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.channel_busy_slot[4] === true,
'btn-outline-secondary': state.channel_busy_slot[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="card-body w-100 h-100 overflow-auto p-2">
<div class="tab-content h-100 w-100" id="nav-stats-tabContent">
<div
class="tab-pane fade h-100 w-100"
v-bind:class="{
'show active': localSpectrumView == 'waterfall',
}"
role="stats_tabpanel"
aria-labelledby="list-waterfall-list"
>
<canvas v-bind:id="localSpectrumID" class="force-gpu"></canvas>
</div>
<div
class="tab-pane fade h-100 w-100"
v-bind:class="{
'show active': localSpectrumView == 'scatter',
}"
role="tabpanel"
aria-labelledby="list-scatter-list"
>
<Scatter :data="scatterChartData" :options="scatterChartOptions" />
</div>
<div
class="tab-pane fade h-100 w-100"
v-bind:class="{ 'show active': localSpectrumView == 'chart' }"
role="tabpanel"
aria-labelledby="list-chart-list"
>
<Line
:data="transmissionSpeedChartData"
:options="transmissionSpeedChartOptions"
/>
</div>
</div>
<!--278px-->
</div>
</div>
</template>

View file

@ -0,0 +1,43 @@
<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);
function getDateTime(timestampRaw) {
var datetime = new Date(timestampRaw * 1000).toLocaleString(
navigator.language,
{
hourCycle: "h23",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
},
);
return datetime;
}
</script>
<template>
<div class="card h-100">
<div class="card-header p-0">
<i class="bi bi-card-list" style="font-size: 1.2rem"></i>&nbsp;
<strong>Activity</strong>
</div>
<div class="card-body overflow-auto m-0 p-0" style="align-items: start">
<div v-for="item in state.activities" :key="item[0]">
<h6 style="text-align: start" class="mb-0">
{{ item[1].origin }} -
<span>{{ getDateTime(item[1].timestamp) }}</span>
</h6>
<p class="mb-2" style="text-align: start; font-size: smaller">
{{ item[1].activity_type }} - {{ item[1].direction }}
</p>
</div>
</div>
</div>
</template>

View file

@ -0,0 +1,36 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../../store/index";
setActivePinia(pinia);
import { setModemBeacon } from "../../js/api.js";
import { useStateStore } from "../../store/stateStore.js";
const state = useStateStore(pinia);
function startStopBeacon() {
if (state.beacon_state === true) {
setModemBeacon(false);
} else {
setModemBeacon(true);
}
}
</script>
<template>
<div class="fill h-100" style="width: calc(100% - 24px)">
<a
class="btn btn-sm btn-secondary d-flex justify-content-center align-items-center object-fill border rounded w-100 h-100"
@click="startStopBeacon"
title="Enable/disable periodic beacons"
>Beacon&nbsp;
<span
class=""
role="status"
v-bind:class="{
'spinner-grow spinner-grow-sm': state.beacon_state === true,
disabled: state.beacon_state === false,
}"
>
</span>
</a>
</div>
</template>

View file

@ -0,0 +1,61 @@
<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>
<div class="progress mb-0 me-4 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 me-4 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>
</template>

View file

@ -0,0 +1,17 @@
<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>
<a
class="d-flex justify-content-center align-items-center object-fill border rounded w-100 h-100 link-underline link-underline-opacity-0 link-underline-opacity-75-hover text-bg-light"
data-bs-toggle="offcanvas"
data-bs-target="#offcanvasFrequency"
>
{{ state.frequency / 1000 }} kHz
</a>
</template>

View file

@ -0,0 +1,30 @@
<script setup lang="ts">
import { reactive, ref } from "vue";
import { setActivePinia } from "pinia";
import { setConfig } from "../../js/api";
import pinia from "../../store/index";
setActivePinia(pinia);
import { settingsStore } from "../../store/settingsStore.js";
</script>
<template>
<div class="w-100">
<div class="input-group input-group-sm" style="width: calc(100% - 24px)">
<input
type="text"
class="form-control"
disabled
style="min-width: 3em; background-color: transparent"
v-model="settingsStore.remote.STATION.mycall"
/>
<span class="input-group-text">-</span>
<input
type="text"
class="form-control"
disabled
style="min-width: 2em; max-width: 2.5em; background-color: transparent"
v-model="settingsStore.remote.STATION.myssid"
/>
</div>
</div>
</template>

View file

@ -0,0 +1,20 @@
<script setup lang="ts">
import { reactive, ref } from "vue";
import { setActivePinia } from "pinia";
import { setConfig } from "../../js/api";
import pinia from "../../store/index";
setActivePinia(pinia);
import { settingsStore } from "../../store/settingsStore.js";
</script>
<template>
<div
class="d-flex justify-content-center align-items-center object-fill border rounded w-100 h-100"
>
<h2>
{{ settingsStore.remote.STATION.mycall }}-{{
settingsStore.remote.STATION.myssid
}}
</h2>
</div>
</template>

View file

@ -0,0 +1,44 @@
<script setup lang="ts">
import { ref } from "vue";
import { setActivePinia } from "pinia";
import pinia from "../../store/index";
setActivePinia(pinia);
import { useStateStore } from "../../store/stateStore.js";
import { sendModemPing } from "../../js/api.js";
const state = useStateStore(pinia);
function transmitPing() {
sendModemPing(dxcallPing.value.toUpperCase());
}
var dxcallPing = ref("");
</script>
<template>
<div class="input-group" style="width: calc(100% - 24px)">
<input
type="text"
class="form-control"
style="min-width: 3rem; text-transform: uppercase; height: 31px"
placeholder="DXcall"
pattern="[A-Z]*"
maxlength="11"
aria-label="Input group"
aria-describedby="btnGroupAddon"
v-model="dxcallPing"
/>
<a
class="btn btn-sm btn-secondary"
style="max-width: 3em"
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
</a>
</div>
</template>

View file

@ -0,0 +1,16 @@
<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>
<div
class="d-flex justify-content-center align-items-center object-fill border rounded w-100 h-100"
:class="state.ptt_state === true ? 'text-bg-warning' : 'text-bg-white'"
>
<h2>ON AIR</h2>
</div>
</template>

View file

@ -0,0 +1,61 @@
<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>
<div class="progress mb-0 me-4 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 me-4 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>
</template>

View file

@ -0,0 +1,91 @@
<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 { computed, onMounted } from "vue";
import { setActivePinia } from "pinia";
import pinia from "../../store/index";
setActivePinia(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 { Scatter } from "vue-chartjs";
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
);
// https://www.chartjs.org/docs/latest/samples/line/segments.html
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: true,
},
},
y: {
type: "linear",
position: "left",
grid: {
display: true,
lineWidth: 1, // Set the line width for y-axis grid lines
},
ticks: {
display: true,
},
},
},
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
},
},
};
const scatterChartData = computed(() => ({
datasets: [
{
type: "scatter",
fill: true,
data: state.scatter,
label: "Scatter",
tension: 0.1,
borderColor: "rgb(0, 255, 0)",
},
],
}));
</script>
<template>
<div class="w-100 h-100">
<Scatter :data="scatterChartData" :options="scatterChartOptions" />
</div>
<!--278px-->
</template>

View file

@ -0,0 +1,25 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../../store/index";
setActivePinia(pinia);
import { stopTransmission } from "../../js/api";
function stopAllTransmissions() {
console.log("stopping transmissions");
stopTransmission();
}
</script>
<template>
<a
class="btn btn-outline-danger d-flex border justify-content-center align-items-center object-fill rounded w-100 h-100"
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 h1"></i>
</a>
</template>

View file

@ -0,0 +1,18 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../../store/index";
setActivePinia(pinia);
</script>
<template>
<div class="fill h-100" style="width: calc(100% - 24px)">
<a
class="btn btn-sm btn-secondary d-flex justify-content-center align-items-center object-fill border rounded w-100 h-100"
data-bs-trigger="hover"
data-bs-html="false"
data-bs-toggle="modal"
data-bs-target="#audioModal"
title="Tune"
>Tune</a
>
</div>
</template>

View file

@ -5,10 +5,7 @@ import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
const version = import.meta.env.PACKAGE_VERSION;
import infoScreen_updater from "./infoScreen_updater.vue";
function openWebExternal(url) {
open(url);
@ -17,67 +14,73 @@ const cards = ref([
{
titleName: "Simon",
titleCall: "DJ2LS",
role: "Founder & Core developer",
role: "Founder & Core Developer",
imgSrc: "dj2ls.png",
},
{
titleName: "Alan",
titleCall: "N1QM",
role: "developer",
role: "Developer",
imgSrc: "person-fill.svg",
},
{
titleName: "Stefan",
titleCall: "DK5SM",
role: "tester",
role: "Tester",
imgSrc: "person-fill.svg",
},
{
titleName: "Wolfgang",
titleCall: "DL4IAZ",
role: "supporter",
role: "Supporter",
imgSrc: "person-fill.svg",
},
{
titleName: "David",
titleCall: "VK5DGR",
role: "codec2 founder",
role: "Codec 2 Founder",
imgSrc: "vk5dgr.jpeg",
},
{
titleName: "John",
titleCall: "EI7IG",
role: "tester",
role: "Tester",
imgSrc: "ei7ig.jpeg",
},
{
titleName: "Paul",
titleCall: "N2KIQ",
role: "developer",
role: "Developer",
imgSrc: "person-fill.svg",
},
{
titleName: "Trip",
titleCall: "KT4WO",
role: "tester",
role: "Tester",
imgSrc: "kt4wo.png",
},
{
titleName: "Manuel",
titleCall: "DF7MH",
role: "tester",
role: "Tester",
imgSrc: "person-fill.svg",
},
{
titleName: "Darren",
titleCall: "G0HWW",
role: "tester",
role: "Tester",
imgSrc: "person-fill.svg",
},
{
titleName: "Kai",
titleCall: "LA3QMA",
role: "developer",
role: "Developer",
imgSrc: "person-fill.svg",
},
{
titleName: "Pedro",
titleCall: "F4JAW",
role: "Core Developer",
imgSrc: "person-fill.svg",
},
]);
@ -91,16 +94,9 @@ onMounted(shuffleCards);
</script>
<template>
<h3 class="m-2">
<span class="badge bg-secondary">FreeDATA: {{ version }}</span>
<span class="ms-2 badge bg-secondary"
>Modem: {{ state.modem_version }}</span
>
</h3>
<!--<infoScreen_updater />-->
<div class="container-fluid">
<div class="row mt-2">
<hr />
<div class="row">
<h6>Important URLs</h6>
<div
@ -184,19 +180,19 @@ onMounted(shuffleCards);
</div>
</div>
</div>
<div class="row mt-5">
<hr />
<h6>Special thanks to</h6>
<hr />
<div class="row">
<h6>We would like to especially thank the following</h6>
</div>
<div class="d-flex flex-nowrap overflow-x-auto vh-100">
<div class="row row-cols-1 row-cols-md-6 g-4 h-100">
<div
class="d-flex flex-nowrap overflow-y-auto w-100"
style="height: calc(100vh - 170px); overflow-x: hidden"
>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 row-cols-lg-6">
<div class="d-inline-block" v-for="card in cards" :key="card.titleName">
<div class="col">
<div
class="card border-dark mb-3 ms-1 me-1"
style="max-width: 15rem"
>
<div class="card border-dark m-2" style="max-width: 15rem">
<img :src="card.imgSrc" class="card-img-top grayscale" />
<div class="card-body">
<p class="card-text text-center">{{ card.role }}</p>

View file

@ -3,12 +3,27 @@ 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";
import { settingsStore } from "../store/settingsStore";
import { onMounted } from "vue";
import { ipcRenderer } from "electron";
const state = useStateStore(pinia);
onMounted(() => {
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");
});
});
});
</script>
<template>
<div class="card mb-0">
<div class="card m-2">
<div class="card-header p-1 d-flex">
<div class="container">
<div class="row">
@ -55,7 +70,7 @@ const settings = useSettingsStore(pinia);
type="button"
disabled
>
{{ settings.update_channel }}
Update channel:&nbsp; {{ settingsStore.local.update_channel }}
</button>
<button
class="btn btn-secondary btn-sm ms-1"

View file

@ -5,10 +5,7 @@ setActivePinia(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";
@ -20,8 +17,10 @@ import main_active_audio_level from "./main_active_audio_level.vue";
import chat from "./chat.vue";
import infoScreen from "./infoScreen.vue";
import main_modem_healthcheck from "./main_modem_healthcheck.vue";
import Dynamic_components from "./dynamic_components.vue";
import { stopTransmission } from "../js/sock.js";
import { stopTransmission } from "../js/api";
function stopAllTransmissions() {
console.log("stopping transmissions");
@ -55,16 +54,19 @@ function stopAllTransmissions() {
role="tablist"
style="margin-top: 100px"
>
<main_modem_healthcheck />
<a
class="list-group-item list-group-item-dark list-group-item-action border-0 rounded-3 mb-2 active"
id="list-modem-list"
id="list-grid-list"
data-bs-toggle="list"
href="#list-modem"
href="#list-grid"
role="tab"
aria-controls="list-modem"
title="Home"
><i class="bi bi-house-door-fill h3"></i
aria-controls="list-grid"
title="Grid"
><i class="bi bi-grid h3"></i
></a>
<a
class="list-group-item list-group-item-dark list-group-item-action border-0 rounded-3 mb-2"
id="list-chat-list"
@ -106,7 +108,6 @@ function stopAllTransmissions() {
aria-controls="list-logger"
><i class="bi bi-activity h3"></i
></a>
<a
class="list-group-item list-group-item-dark list-group-item-action border-0 rounded-3 mb-2"
id="list-settings-list"
@ -117,33 +118,24 @@ function stopAllTransmissions() {
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 -->
<!-- TODO: Remove the top navbar entirely if not needed
<main_top_navbar />
-->
<div class="tab-content" id="nav-tabContent-settings">
<div
class="tab-pane fade show active"
id="list-modem"
class="tab-pane fade"
id="list-home"
role="tabpanel"
aria-labelledby="list-modem-list"
aria-labelledby="list-home-list"
>
<!-- TOP NAVBAR -->
<main_top_navbar />
<div
id="blurdiv"
style="
@ -156,51 +148,25 @@ function stopAllTransmissions() {
<!-------------------------------- 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="row">
<div class="col-5">
<main_active_audio_level />
<main_active_rig_control />
</div>
<div class="col">
<div class="col-4">
<main_active_broadcasts />
</div>
<div class="col-3">
<main_active_audio_level />
</div>
</div>
<div
class="row collapse multi-collapse mt-3"
id="collapseFourthRow"
>
<div class="row">
<div class="col-7">
<main_active_heard_stations />
</div>
<div class="col-5">
<main_active_stats />
</div>
<div class="col">
<main_active_heard_stations />
</div>
</div>
</div>
</div>
@ -349,6 +315,15 @@ function stopAllTransmissions() {
>
<infoScreen />
</div>
<div
class="tab-pane fade show active"
id="list-grid"
role="tabpanel"
aria-labelledby="list-grid-list"
>
<Dynamic_components />
</div>
<div
class="tab-pane fade"
id="list-chat"

View file

@ -5,12 +5,6 @@ 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">
@ -21,7 +15,7 @@ function startStopRecordAudio() {
<i class="bi bi-volume-up" style="font-size: 1.2rem"></i>
</div>
<div class="col-3">
<strong class="fs-5">Audio</strong>
<strong>Audio</strong>
</div>
<div class="col-7">
<button
@ -33,18 +27,6 @@ function startStopRecordAudio() {
>
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

View file

@ -1,37 +1,26 @@
<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 { settingsStore as settings} from "../store/settingsStore.js";
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
import { sendCQ, sendPing, startBeacon, stopBeacon } from "../js/sock.js";
function transmitCQ() {
sendCQ();
}
import { sendModemCQ, sendModemPing, setModemBeacon } from "../js/api.js";
function transmitPing() {
sendPing((<HTMLInputElement>document.getElementById("dxCall")).value);
sendModemPing((<HTMLInputElement>document.getElementById("dxCall")).value);
}
function startStopBeacon() {
switch (state.beacon_state) {
case "False":
startBeacon(settings.beacon_interval);
break;
case "True":
stopBeacon();
break;
default:
if (state.beacon_state === true) {
setModemBeacon(false);
}
else {
setModemBeacon(true);
}
}
</script>
@ -94,7 +83,7 @@ function startStopBeacon() {
id="sendCQ"
type="button"
title="Send a CQ to the world"
@click="transmitCQ()"
@click="sendModemCQ()"
>
Call CQ
</button>
@ -105,8 +94,8 @@ function startStopBeacon() {
class="btn btn-sm ms-1"
@click="startStopBeacon()"
v-bind:class="{
'btn-success': state.beacon_state === 'True',
'btn-outline-secondary': state.beacon_state === 'False',
'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."
>

View file

@ -6,8 +6,7 @@ import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { settingsStore as settings } from "../store/settingsStore.js";
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
@ -30,9 +29,9 @@ function getDateTime(timestampRaw) {
function getMaidenheadDistance(dxGrid) {
try {
return parseInt(distance(settings.mygrid, dxGrid));
return parseInt(distance(settings.remote.STATION.mygrid, dxGrid));
} catch (e) {
//
console.warn(e);
}
}
</script>
@ -82,7 +81,7 @@ function getMaidenheadDistance(dxGrid) {
</thead>
<tbody id="heardstations">
<!--https://vuejs.org/guide/essentials/list.html-->
<tr v-for="item in state.heard_stations" :key="item.timestamp">
<tr v-for="item in state.heard_stations" :key="item.origin">
<td>
<span class="badge bg-secondary">{{
getDateTime(item.timestamp)
@ -94,18 +93,18 @@ function getMaidenheadDistance(dxGrid) {
>
</td>
<td>
<span class="badge bg-secondary">{{ item.dxcallsign }}</span>
<span class="badge bg-secondary">{{ item.origin }}</span>
</td>
<td>
<span class="badge bg-secondary">{{ item.dxgrid }}</span>
<span class="badge bg-secondary">{{ item.gridsquare }}</span>
</td>
<td>
<span class="badge bg-secondary"
>{{ getMaidenheadDistance(item.dxgrid) }} km</span
>{{ getMaidenheadDistance(item.gridsquare) }} km</span
>
</td>
<td>
<span class="badge bg-secondary">{{ item.datatype }}</span>
<span class="badge bg-secondary">{{ item.activity_type }}</span>
</td>
<td>
<span class="badge bg-secondary">{{ item.snr }}</span>

View file

@ -6,23 +6,15 @@ setActivePinia(pinia);
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
import { set_frequency, set_mode, set_rf_level } from "../js/sock.js";
import { setRadioParameters } from "../js/api";
function updateFrequencyAndApply(frequency) {
state.new_frequency = frequency;
set_frequency(state.new_frequency);
set_radio_parameters();
}
function set_hamlib_frequency_manually() {
set_frequency(state.new_frequency);
}
function set_hamlib_mode() {
set_mode(state.mode);
}
function set_hamlib_rf_level() {
set_rf_level(state.rf_level);
function set_radio_parameters() {
setRadioParameters(state.new_frequency, state.mode, state.rf_level);
}
</script>
@ -79,9 +71,7 @@ function set_hamlib_rf_level() {
id="dropdownMenuButton"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Select Frequency
</button>
></button>
<!-- Dropdown Menu -->
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton">
@ -104,7 +94,7 @@ function set_hamlib_rf_level() {
<button
class="btn btn-sm btn-outline-success"
type="button"
@click="set_hamlib_frequency_manually"
@click="updateFrequencyAndApply(state.new_frequency)"
v-bind:class="{
disabled: state.hamlib_status === 'disconnected',
}"
@ -217,7 +207,7 @@ function set_hamlib_rf_level() {
<select
class="form-control"
v-model="state.mode"
@click="set_hamlib_mode()"
@click="set_radio_parameters()"
v-bind:class="{
disabled: state.hamlib_status === 'disconnected',
}"
@ -239,7 +229,7 @@ function set_hamlib_rf_level() {
<select
class="form-control"
v-model="state.rf_level"
@click="set_hamlib_rf_level()"
@click="set_radio_parameters()"
v-bind:class="{
disabled: state.hamlib_status === 'disconnected',
}"

View file

@ -2,14 +2,11 @@
// @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 { settingsStore as settings } from "../store/settingsStore.js";
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
@ -30,18 +27,18 @@ import { computed } from "vue";
function selectStatsControl(obj) {
switch (obj.delegateTarget.id) {
case "list-waterfall-list":
settings.spectrum = "waterfall";
settings.local.spectrum = "waterfall";
break;
case "list-scatter-list":
settings.spectrum = "scatter";
settings.local.spectrum = "scatter";
break;
case "list-chart-list":
settings.spectrum = "chart";
settings.local.spectrum = "chart";
break;
default:
settings.spectrum = "waterfall";
settings.local.spectrum = "waterfall";
}
saveSettingsToFile();
//saveSettingsToFile();
}
ChartJS.register(
@ -172,8 +169,8 @@ const scatterChartData = computed(() => ({
</script>
<script lang="ts">
import { initWaterfall } from "../js/waterfallHandler.js";
import { initWaterfall, setColormap } from "../js/waterfallHandler.js";
var localSpectrum;
export default {
mounted() {
// This code will be executed after the component is mounted to the DOM
@ -181,7 +178,7 @@ export default {
//const myElement = this.$refs.waterfall; // Access the DOM element with ref
// init waterfall
initWaterfall();
localSpectrum = initWaterfall("waterfall-main");
},
};
</script>
@ -205,7 +202,9 @@ export default {
href="#list-waterfall"
role="tab"
aria-controls="list-waterfall"
v-bind:class="{ active: settings.spectrum === 'waterfall' }"
v-bind:class="{
active: settings.local.spectrum === 'waterfall',
}"
@click="selectStatsControl($event)"
><strong><i class="bi bi-water"></i></strong
></a>
@ -216,7 +215,9 @@ export default {
href="#list-scatter"
role="tab"
aria-controls="list-scatter"
v-bind:class="{ active: settings.spectrum === 'scatter' }"
v-bind:class="{
active: settings.local.spectrum === 'scatter',
}"
@click="selectStatsControl($event)"
><strong><i class="bi bi-border-outer"></i></strong
></a>
@ -227,7 +228,7 @@ export default {
href="#list-chart"
role="tab"
aria-controls="list-chart"
v-bind:class="{ active: settings.spectrum === 'chart' }"
v-bind:class="{ active: settings.local.spectrum === 'chart' }"
@click="selectStatsControl($event)"
><strong><i class="bi bi-graph-up-arrow"></i></strong
></a>
@ -242,9 +243,8 @@ export default {
data-bs-trigger="hover"
data-bs-html="true"
v-bind:class="{
'btn-warning': state.getChannelBusySlotState(0) === true,
'btn-outline-secondary':
state.getChannelBusySlotState(0) === false,
'btn-warning': state.channel_busy_slot[0] === true,
'btn-outline-secondary': state.channel_busy_slot[0] === false,
}"
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
>
@ -259,9 +259,8 @@ export default {
data-bs-trigger="hover"
data-bs-html="true"
v-bind:class="{
'btn-warning': state.getChannelBusySlotState(1) === true,
'btn-outline-secondary':
state.getChannelBusySlotState(1) === false,
'btn-warning': state.channel_busy_slot[1] === true,
'btn-outline-secondary': state.channel_busy_slot[1] === false,
}"
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
>
@ -276,9 +275,8 @@ export default {
data-bs-trigger="hover"
data-bs-html="true"
v-bind:class="{
'btn-warning': state.getChannelBusySlotState(2) === true,
'btn-outline-secondary':
state.getChannelBusySlotState(2) === false,
'btn-warning': state.channel_busy_slot[2] === true,
'btn-outline-secondary': state.channel_busy_slot[2] === false,
}"
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
>
@ -293,9 +291,8 @@ export default {
data-bs-trigger="hover"
data-bs-html="true"
v-bind:class="{
'btn-warning': state.getChannelBusySlotState(3) === true,
'btn-outline-secondary':
state.getChannelBusySlotState(3) === false,
'btn-warning': state.channel_busy_slot[3] === true,
'btn-outline-secondary': state.channel_busy_slot[3] === false,
}"
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
>
@ -310,9 +307,8 @@ export default {
data-bs-trigger="hover"
data-bs-html="true"
v-bind:class="{
'btn-warning': state.getChannelBusySlotState(4) === true,
'btn-outline-secondary':
state.getChannelBusySlotState(4) === false,
'btn-warning': state.channel_busy_slot[4] === true,
'btn-outline-secondary': state.channel_busy_slot[4] === false,
}"
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
>
@ -328,8 +324,8 @@ export default {
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',
'btn-success': state.is_codec2_traffic === true,
'btn-outline-secondary': state.is_codec2_traffic === false,
}"
>
data
@ -355,21 +351,31 @@ export default {
<div class="tab-content" id="nav-stats-tabContent">
<div
class="tab-pane fade"
v-bind:class="{ 'show active': settings.spectrum === 'waterfall' }"
v-bind:class="{
'show active': settings.local.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"
ref="waterfall-main"
id="waterfall-main"
style="
position: relative;
z-index: 2;
aspect-ratio: unset;
width: 100%;
height: 200px;
"
class="force-gpu'"
></canvas>
</div>
<div
class="tab-pane fade"
v-bind:class="{ 'show active': settings.spectrum === 'scatter' }"
v-bind:class="{
'show active': settings.local.spectrum === 'scatter',
}"
id="list-scatter"
role="tabpanel"
aria-labelledby="list-scatter-list"
@ -378,7 +384,7 @@ export default {
</div>
<div
class="tab-pane fade"
v-bind:class="{ 'show active': settings.spectrum === 'chart' }"
v-bind:class="{ 'show active': settings.local.spectrum === 'chart' }"
id="list-chart"
role="tabpanel"
aria-labelledby="list-chart-list"

View file

@ -5,6 +5,8 @@ setActivePinia(pinia);
import { useAudioStore } from "../store/audioStore.js";
const audio = useAudioStore(pinia);
import { setConfig } from "../js/api";
</script>
<template>
@ -21,7 +23,6 @@ const audio = useAudioStore(pinia);
<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"
@ -40,9 +41,9 @@ const audio = useAudioStore(pinia);
</span>
<select
class="form-select form-select-sm"
id="audio_input_selectbox"
aria-label=".form-select-sm"
v-html="audio.getInputDevices()"
@change="setConfig"
></select>
</div>
<div class="input-group input-group-sm">
@ -51,9 +52,9 @@ const audio = useAudioStore(pinia);
</span>
<select
class="form-select form-select-sm"
id="audio_output_selectbox"
aria-label=".form-select-sm"
v-html="audio.getOutputDevices()"
@change="setConfig"
></select>
</div>
</div>

View file

@ -18,8 +18,8 @@ const state = useStateStore(pinia);
<button
class="btn btn-sm btn-secondary me-1"
v-bind:class="{
'btn-danger': state.ptt_state === 'True',
'btn-secondary': state.ptt_state === 'False',
'btn-danger': state.ptt_state == true,
'btn-secondary': state.ptt_state == false,
}"
id="ptt_state"
type="button"
@ -110,8 +110,8 @@ const state = useStateStore(pinia);
data-bs-trigger="hover"
data-bs-html="true"
v-bind:class="{
'btn-warning': state.channel_busy === 'True',
'btn-secondary': state.channel_busy === 'False',
'btn-warning': state.channel_busy === true,
'btn-secondary': state.channel_busy === false,
}"
style="pointer-events: auto"
data-bs-title="Channel busy"
@ -130,7 +130,7 @@ const state = useStateStore(pinia);
data-bs-title="What's the frequency, Kenneth?"
style="pointer-events: auto"
>
{{ parseInt(state.frequency) / 1000 }} KHz
{{ state.frequency / 1000 }} kHz
</button>
</div>
@ -215,10 +215,7 @@ const state = useStateStore(pinia);
</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 w-100" style="height: 20px; min-width: 200px">
<div
class="progress-bar progress-bar-striped bg-primary force-gpu"
id="transmission_progress"
@ -231,11 +228,12 @@ const state = useStateStore(pinia);
<p
class="justify-content-center m-0 d-flex position-absolute w-100 text-dark"
>
{{ state.arq_seconds_until_finish }}s left
Message Progress
</p>
</div>
<div
hidden
class="progress mb-0 rounded-0 rounded-bottom"
style="height: 10px"
>

View file

@ -4,29 +4,20 @@
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { saveSettingsToFile } from "../js/settingsHandler";
import { useChatStore } from "../store/chatStore.js";
const chat = useChatStore(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { settingsStore as settings, onChange } from "../store/settingsStore.js";
import { sendModemTestFrame } from "../js/api";
import {
deleteChatByCallsign,
getNewMessagesByDXCallsign,
} from "../js/chatHandler";
import { sendTestFrame, setTxAudioLevel } from "../js/sock.js";
function tuneAudio() {
sendTestFrame();
}
function set_audio_level() {
saveSettingsToFile();
setTxAudioLevel(settings.tx_audio_level);
}
import main_startup_check from "./main_startup_check.vue";
function deleteChat() {
//console.log(chat.selectedCallsign)
@ -121,6 +112,8 @@ const transmissionSpeedChartDataMessageInfo = computed(() => ({
</script>
<template>
<main_startup_check />
<!-- updater release notes-->
<div
class="modal fade"
@ -927,7 +920,7 @@ const transmissionSpeedChartDataMessageInfo = computed(() => ({
<button
type="button"
class="btn btn-sm btn-outline-secondary"
@click="tuneAudio"
@click="sendModemTestFrame()"
>
Tune
</button>
@ -1183,30 +1176,53 @@ const transmissionSpeedChartDataMessageInfo = computed(() => ({
></button>
</div>
<div class="modal-body">
<div class="alert alert-info" role="alert">
Adjust audio levels. Value in dB. Default is <strong>0</strong>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text">Test-Frame</span>
<button
type="button"
id="sendTestFrame"
@click="sendTestFrame()"
@click="sendModemTestFrame()"
class="btn btn-danger"
>
Transmit
</button>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text">TX Level</span>
<span class="input-group-text">{{ settings.tx_audio_level }}</span>
<span class="input-group-text">RX Level</span>
<span class="input-group-text">{{
settings.remote.AUDIO.rx_audio_level
}}</span>
<span class="input-group-text w-75">
<input
type="range"
class="form-range"
min="0"
max="250"
min="-30"
max="20"
step="1"
id="audioLevelRX"
@change="onChange"
v-model.number="settings.remote.AUDIO.rx_audio_level"
/></span>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text">TX Level</span>
<span class="input-group-text">{{
settings.remote.AUDIO.tx_audio_level
}}</span>
<span class="input-group-text w-75">
<input
type="range"
class="form-range"
min="-30"
max="20"
step="1"
id="audioLevelTX"
@click="set_audio_level()"
v-model="settings.tx_audio_level"
@change="onChange"
v-model.number="settings.remote.AUDIO.tx_audio_level"
/></span>
</div>
</div>

View file

@ -0,0 +1,35 @@
<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);
function getOverallHealth() {
//Return a number indicating health for icon bg color; lower the number the healthier
let health = 0;
if (state.modem_connection !== "connected") health += 5;
if (!state.is_modem_running) health += 3;
if (state.radio_status === false) health += 2;
if (process.env.FDUpdateAvail === "1") health += 1;
return health;
}
</script>
<template>
<a
class="btn border btn-outline-secondary list-group-item mb-5"
data-bs-html="false"
data-bs-toggle="modal"
data-bs-target="#modemCheck"
title="Check FreeDATA status"
:class="
getOverallHealth() > 4
? 'bg-danger'
: getOverallHealth() < 2
? ''
: 'bg-warning'
"
><i class="bi bi-activity h3"></i>
</a>
</template>

View file

@ -1,16 +1,9 @@
<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();
}
import { settingsStore as settings } from "../store/settingsStore.js";
</script>
<template>
@ -62,15 +55,13 @@ function saveSettings() {
maxlength="8"
aria-label="Input group"
aria-describedby="btnGroupAddon"
v-model="settings.mycall"
@input="saveSettings"
v-model="settings.remote.STATION.mycall"
/>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="myCallSSID"
v-model="settings.myssid"
@change="saveSettings"
v-model="settings.remote.STATION.myssid"
>
<option selected value="0">0</option>
<option value="1">1</option>
@ -112,8 +103,7 @@ function saveSettings() {
maxlength="6"
aria-label="Input group"
aria-describedby="btnGroupAddon"
v-model="settings.mygrid"
@input="saveSettings"
v-model="settings.remote.STATION.mygrid"
/>
</div>
</div>

View file

@ -1,15 +1,11 @@
<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 { settingsStore as settings} from "../store/settingsStore.js";
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
@ -18,15 +14,15 @@ function startStopRigctld() {
switch (state.rigctld_started) {
case "stopped":
settings.hamlib_deviceport = (<HTMLInputElement>document.getElementById("hamlib_deviceport")).value;
settings.remote.RADIO.serial_port = (<HTMLInputElement>document.getElementById("hamlib_deviceport")).value;
startRigctld();
//startRigctld();
break;
case "running":
stopRigctld();
//stopRigctld();
// dirty hack for calling this command twice, otherwise modem won't stop rigctld from time to time
stopRigctld();
//stopRigctld();
break;
default:
}
@ -36,19 +32,19 @@ function selectRadioControl() {
// @ts-expect-error
switch (event.target.id) {
case "list-rig-control-none-list":
settings.radiocontrol = "disabled";
settings.remote.RADIO.control = "disabled";
break;
case "list-rig-control-rigctld-list":
settings.radiocontrol = "rigctld";
settings.remote.RADIO.control = "rigctld";
break;
case "list-rig-control-tci-list":
settings.radiocontrol = "tci";
settings.remote.RADIO.control = "tci";
break;
default:
console.log("default=!==");
settings.radiocontrol = "disabled";
settings.remote.RADIO.control = "disabled";
}
saveSettingsToFile();
//saveSettingsToFile();
}
@ -61,8 +57,9 @@ alert("not yet implemented")
</script>
<template>
<div class="card mb-0">
<div class="card-header p-1">
<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">
@ -85,7 +82,7 @@ alert("not yet implemented")
href="#list-rig-control-none"
role="tab"
aria-controls="list-rig-control-none"
v-bind:class="{ active: settings.radiocontrol === 'disabled' }"
v-bind:class="{ active: settings.remote.RADIO.control === 'disabled' }"
@click="selectRadioControl()"
>None</a
>
@ -96,7 +93,7 @@ alert("not yet implemented")
href="#list-rig-control-rigctld"
role="tab"
aria-controls="list-rig-control-rigctld"
v-bind:class="{ active: settings.radiocontrol === 'rigctld' }"
v-bind:class="{ active: settings.remote.RADIO.control === 'rigctld' }"
@click="selectRadioControl()"
>Rigctld</a
>
@ -107,7 +104,7 @@ alert("not yet implemented")
href="#list-rig-control-tci"
role="tab"
aria-controls="list-rig-control-tci"
v-bind:class="{ active: settings.radiocontrol === 'tci' }"
v-bind:class="{ active: settings.remote.RADIO.control === 'tci' }"
@click="selectRadioControl()"
>TCI</a
>
@ -132,7 +129,7 @@ alert("not yet implemented")
<div class="tab-content" id="rig-control-nav-tabContent">
<div
class="tab-pane fade"
v-bind:class="{ 'show active': settings.radiocontrol === 'disabled' }"
v-bind:class="{ 'show active': settings.remote.RADIO.control === 'disabled' }"
id="list-rig-control-none"
role="tabpanel"
aria-labelledby="list-rig-control-none-list"
@ -146,27 +143,11 @@ alert("not yet implemented")
<div
class="tab-pane fade"
id="list-rig-control-rigctld"
v-bind:class="{ 'show active': settings.radiocontrol === 'rigctld' }"
v-bind:class="{ 'show active': settings.remote.RADIO.control === '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>
@ -217,7 +198,7 @@ alert("not yet implemented")
<div
class="tab-pane fade"
id="list-rig-control-tci"
v-bind:class="{ 'show active': settings.radiocontrol === 'tci' }"
v-bind:class="{ 'show active': settings.remote.RADIO.control === 'tci' }"
role="tabpanel"
aria-labelledby="list-rig-control-tci-list"
>
@ -231,7 +212,7 @@ alert("not yet implemented")
placeholder="tci IP"
id="tci_ip"
aria-label="Device IP"
v-model="settings.tci_ip"
v-model="settings.remote.TCI.tci_ip"
/>
</div>
@ -243,7 +224,7 @@ alert("not yet implemented")
placeholder="tci port"
id="tci_port"
aria-label="Device Port"
v-model="settings.tci_port"
v-model="settings.remote.TCI.tci_port"
/>
</div>
</div>
@ -275,4 +256,5 @@ alert("not yet implemented")
</div>
-->
</div>
</div>
</template>

View file

@ -0,0 +1,484 @@
<script setup lang="ts">
import { Modal } from "bootstrap";
import { onMounted } from "vue";
import infoScreen_updater from "./infoScreen_updater.vue";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { settingsStore as settings, onChange } from "../store/settingsStore.js";
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
import {
getVersion,
setConfig,
startModem,
stopModem,
getModemState,
} from "../js/api";
import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
import { serialDeviceOptions } from "../js/deviceFormHelper";
const version = import.meta.env.PACKAGE_VERSION;
var updateAvailable = process.env.FDUpdateAvail;
// start modemCheck modal once on startup
onMounted(() => {
getVersion().then((res) => {
state.modem_version = res;
});
new Modal("#modemCheck", {}).show();
});
function refreshModem() {
getModemState();
}
function getModemStateLocal() {
// Returns active/inactive if modem is running for modem status label
if (state.is_modem_running == true) return "Active";
else return "Inactive";
}
function getNetworkState() {
// Returns active/inactive if modem is running for modem status label
if (state.modem_connection === "connected") return "Connected";
else return "Disconnected";
}
function getRigControlStuff() {
switch (settings.remote.RADIO.control) {
case "disabled":
return true;
case "rigctld":
case "tci":
return state.radio_status;
default:
console.error(
"Unknown radio control mode " + settings.remote.RADIO.control,
);
return "Unknown control type" + settings.remote.RADIO.control;
}
}
function testHamlib() {
alert("Not yet implemented.");
}
</script>
<template>
<div
class="modal modal-lg fade"
id="modemCheck"
data-bs-backdrop="static"
data-bs-keyboard="false"
tabindex="-1"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5">Modem check</h1>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div class="accordion" id="startupCheckAccordion">
<!-- Network Section -->
<div class="accordion-item">
<h2 class="accordion-header">
<button
class="accordion-button collapsed"
type="button"
data-bs-target="#networkStatusCollapse"
data-bs-toggle="collapse"
>
Network
<span
class="badge ms-2 bg-success"
:class="
state.modem_connection === 'connected'
? 'bg-success'
: 'bg-danger'
"
>{{ getNetworkState() }}</span
>
</button>
</h2>
<div
id="networkStatusCollapse"
class="accordion-collapse collapse"
>
<div class="accordion-body">
<div class="input-group input-group-sm mb-1">
<span class="input-group-text w-25">Modem port</span>
<input
type="text"
class="form-control"
placeholder="modem port (def 5000)"
id="modem_port"
maxlength="5"
max="65534"
min="1025"
v-model="settings.local.port"
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text w-25">Modem host</span>
<input
type="text"
class="form-control"
placeholder="modem host (default 127.0.0.1)"
id="modem_port"
v-model="settings.local.host"
/>
</div>
</div>
</div>
</div>
<!-- Modem Section -->
<div class="accordion-item">
<h2 class="accordion-header">
<button
class="accordion-button collapsed"
type="button"
data-bs-target="#modemStatusCollapse"
data-bs-toggle="collapse"
>
Modem
<span
class="badge ms-2"
:class="
state.is_modem_running === true
? 'bg-success'
: 'bg-danger'
"
>{{ getModemStateLocal() }}</span
>
</button>
</h2>
<div id="modemStatusCollapse" class="accordion-collapse collapse">
<div class="accordion-body">
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-25">Modem control</label>
<label class="input-group-text">
<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="startModem"
v-bind:class="{
disabled: state.is_modem_running === true,
}"
>
<i class="bi bi-play-fill"></i>
</button> </label
><label class="input-group-text">
<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="stopModem"
v-bind:class="{
disabled: state.is_modem_running === false,
}"
>
<i class="bi bi-stop-fill"></i>
</button>
</label>
<label class="input-group-text">
<button
type="button"
id="refreshModem"
class="btn btn-sm btn-outline-secondary"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="false"
title="Refresh modem status."
@click="refreshModem"
>
<i class="bi bi-bullseye"></i>
</button>
</label>
</div>
<!-- Audio Input Device -->
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50"
>Audio Input device</label
>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
@change="onChange"
v-model="settings.remote.AUDIO.input_device"
>
<option
v-for="option in audioInputOptions()"
v-bind:value="option.id"
>
{{ option.name }} [{{ option.api }}]
</option>
</select>
</div>
<!-- Audio Output Device -->
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50"
>Audio Output device</label
>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
@change="onChange"
v-model="settings.remote.AUDIO.output_device"
>
<option
v-for="option in audioOutputOptions()"
v-bind:value="option.id"
>
{{ option.name }} [{{ option.api }}]
</option>
</select>
</div>
</div>
</div>
</div>
<!-- Radio Control Section -->
<div class="accordion-item">
<h2 class="accordion-header">
<button
class="accordion-button collapsed"
type="button"
data-bs-target="#radioControlCollapse"
data-bs-toggle="collapse"
>
Radio control
<span
class="badge ms-2"
:class="
getRigControlStuff() === true ? 'bg-success' : 'bg-danger'
"
>{{
getRigControlStuff() === true ? "Online" : "Offline"
}}</span
>
</button>
</h2>
<div
id="radioControlCollapse"
class="accordion-collapse collapse"
>
<div class="accordion-body">
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px"
>Rig control method</span
>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="rigcontrol_radiocontrol"
@change="onChange"
v-model="settings.remote.RADIO.control"
>
<option selected value="disabled">
Disabled (no rig control; use with VOX)
</option>
<option selected value="rigctld">Rigctld (Hamlib)</option>
<option selected value="tci">TCI</option>
</select>
</div>
<div
:class="
settings.remote.RADIO.control == 'rigctld' ? '' : 'd-none'
"
>
<!-- Shown when rigctld is selected-->
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-25"
>Rigctld control</label
>
<label class="input-group-text">
<button
type="button"
class="btn btn-sm btn-outline-success"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="false"
title="Start rigctld"
v-bind:class="{
disabled: state.rigctld_started == 'true',
}"
>
<i class="bi bi-play-fill"></i>
</button> </label
><label class="input-group-text">
<button
type="button"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="false"
title="Stop rigctld"
v-bind:class="{
disabled:
state.rigctld_started == 'false' ||
state.rigctld_started === undefined,
}"
>
<i class="bi bi-stop-fill"></i>
</button>
</label>
<label class="input-group-text">
<button
type="button"
id="testHamlib"
class="btn btn-sm btn-outline-secondary"
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>
</label>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px"
>Radio port</span
>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="hamlib_deviceport"
style="width: 7rem"
@change="onChange"
v-model="settings.remote.RADIO.serial_port"
>
<option
v-for="option in serialDeviceOptions()"
v-bind:value="option.port"
>
{{ option.description }}
</option>
</select>
</div>
</div>
<div
:class="
settings.remote.RADIO.control == 'tci' ? '' : 'd-none'
"
>
<!-- Shown when tci is selected-->
<div class="input-group input-group-sm mb-1">
<span class="input-group-text w-25">TCI IP address</span>
<input
type="text"
class="form-control"
placeholder="TCI IP"
id="rigcontrol_tci_ip"
aria-label="Device IP"
v-model="settings.remote.TCI.tci_ip"
@change="onChange"
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text w-25">TCI port</span>
<input
type="text"
class="form-control"
placeholder="TCI port"
id="rigcontrol_tci_port"
aria-label="Device Port"
v-model="settings.remote.TCI.tci_port"
@change="onChange"
/>
</div>
</div>
</div>
</div>
</div>
<!-- Version Section -->
<div class="accordion-item">
<h2 class="accordion-header">
<button
class="accordion-button collapsed"
type="button"
data-bs-target="#versionCheckCollapse"
data-bs-toggle="collapse"
>
Version
<span
class="badge ms-2"
:class="
updateAvailable === '1' ? 'bg-warning' : 'bg-success'
"
>
{{
updateAvailable === "1"
? "Update available ! ! ! !"
: "Current"
}}</span
>
</button>
</h2>
<div
id="versionCheckCollapse"
class="accordion-collapse collapse"
>
<div class="accordion-body">
<button
class="btn btn-secondary btn-sm ms-1 me-1"
type="button"
disabled
>
GUI version | {{ version }}
</button>
<button
class="btn btn-secondary btn-sm ms-1 me-1"
type="button"
disabled
>
Modem version | {{ state.modem_version }}
</button>
<div :class="updateAvailable === '1' ? '' : 'd-none'">
<infoScreen_updater />
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">
Continue
</button>
</div>
</div>
</div>
</div>
</template>

View file

@ -1,5 +1,4 @@
<script setup lang="ts">
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
@ -7,101 +6,21 @@ 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:
}
}
import { settingsStore as settings } from "../store/settingsStore.js";
</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>
<span class="badge bg-secondary me-4">
Modem Connection {{ state.modem_connection }}
</span>
<span class="badge bg-secondary me-4">
Modem {{ state.is_modem_running }}
</span>
<span class="badge bg-secondary me-4">
RIG Control {{ state.rigctld_started }}
</span>
</div>
</nav>
</nav>
</template>

View file

@ -1,7 +1,8 @@
<script setup lang="ts">
import settings_station from "./settings_station.vue";
import settings_gui from "./settings_gui.vue";
import settings_chat from "./settings_chat.vue";
import settings_hamlib from "./settings_hamlib.vue";
import settings_rigcontrol from "./settings_rigcontrol.vue";
import settings_modem from "./settings_modem.vue";
import settings_web from "./settings_web.vue";
import settings_exp from "./settings_exp.vue";
@ -14,13 +15,6 @@ import settings_exp from "./settings_exp.vue";
aria-labelledby="list-settings-list"
>
<div class="container">
<div class="badge text-bg-warning m-1">
<h5>
<i class="bi bi-exclamation-triangle"></i> Please restart the modem
after changing settings <i class="bi bi-exclamation-triangle"></i>
</h5>
</div>
<div class="card text-center">
<div class="card-header">
<!-- SETTINGS Nav tabs -->
@ -28,6 +22,20 @@ import settings_exp from "./settings_exp.vue";
<li class="nav-item" role="presentation">
<button
class="nav-link active"
id="station-tab"
data-bs-toggle="tab"
data-bs-target="#station"
type="button"
role="tab"
aria-controls="home"
aria-selected="true"
>
Station
</button>
</li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
id="gui-tab"
data-bs-toggle="tab"
data-bs-target="#gui"
@ -56,17 +64,18 @@ import settings_exp from "./settings_exp.vue";
<li class="nav-item" role="presentation">
<button
class="nav-link"
id="hamlib-tab"
id="rigcontrol-tab"
data-bs-toggle="tab"
data-bs-target="#hamlib"
data-bs-target="#rigcontrol"
type="button"
role="tab"
aria-controls="profile"
aria-selected="false"
>
Hamlib
Rig Control
</button>
</li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
@ -111,12 +120,27 @@ import settings_exp from "./settings_exp.vue";
</li>
</ul>
</div>
<div class="card-body">
<div
class="card-body overflow-auto"
style="height: calc(100vh - 105px)"
>
<!-- SETTINGS Nav Tab panes -->
<!-- Station tab contents-->
<div class="tab-content">
<!-- GUI tab contents-->
<div
class="tab-pane active"
id="station"
role="tabpanel"
aria-labelledby="station-tab"
tabindex="0"
>
<settings_station />
</div>
<!-- GUI tab contents-->
<div
class="tab-pane"
id="gui"
role="tabpanel"
aria-labelledby="gui-tab"
@ -137,13 +161,14 @@ import settings_exp from "./settings_exp.vue";
<div
class="tab-pane"
id="hamlib"
id="rigcontrol"
role="tabpanel"
aria-labelledby="hamlib-tab"
aria-labelledby="rigcontrol-tab"
tabindex="0"
>
<settings_hamlib />
<settings_rigcontrol />
</div>
<div
class="tab-pane"
id="modem"

View file

@ -1,127 +1,13 @@
<script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler";
import { setConfig } from "../js/api";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
function saveSettings() {
saveSettingsToFile();
}
import { settingsStore as settings } from "../store/settingsStore.js";
</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"
>
<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>
<h5>...soon...</h5>
</template>

View file

@ -1,58 +1,14 @@
<script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler";
import { setConfig } from "../js/api";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
function saveSettings() {
saveSettingsToFile();
}
import { settingsStore as settings } from "../store/settingsStore.js";
</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">
@ -61,10 +17,8 @@ function saveSettings() {
class="form-check-input"
type="checkbox"
id="enableMeshSwitch"
@change="saveSettings"
v-model="settings.enable_mesh_features"
true-value="True"
false-value="False"
@change="setConfig"
v-model="settings.remote.MESH.enable_protocol"
/>
<label class="form-check-label" for="enableMeshSwitch"
>experimental! REALLY!</label
@ -72,24 +26,6 @@ function saveSettings() {
</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

View file

@ -1,65 +1,25 @@
<script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler";
import { setColormap } from "../js/waterfallHandler";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { settingsStore as settings } from "../store/settingsStore.js";
function saveSettings() {
saveSettingsToFile();
//saveSettingsToFile();
setColormap();
}
</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
v-model="settings.local.wf_theme"
>
<option value="2">Default</option>
<option value="0">Turbo</option>
@ -70,49 +30,17 @@ function saveSettings() {
<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
v-model="settings.local.update_channel"
>
<option value="latest">stable</option>
<option value="beta">beta</option>
<option value="alpha">alpha</option>
<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">
@ -124,10 +52,7 @@ function saveSettings() {
type="checkbox"
id="NotificationSwitch"
@change="saveSettings"
v-model="settings.enable_sys_notification"
true-value="True"
false-value="False"
disabled
v-model="settings.local.enable_sys_notification"
/>
<label class="form-check-label" for="NotificationSwitch"
>Show system pop-ups</label
@ -135,24 +60,4 @@ function saveSettings() {
</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

@ -1,351 +1,350 @@
<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();
}
import { settingsStore as settings, onChange } from "../store/settingsStore.js";
import { serialDeviceOptions } from "../js/deviceFormHelper";
</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>
<span class="input-group-text" style="width: 180px">Rigctld 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"
@change="onChange"
v-model="settings.remote.RIGCTLD.ip"
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px"
>Rigctld remote port</span
>
<span class="input-group-text" style="width: 180px">Rigctld 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"
@change="onChange"
v-model="settings.remote.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"
<span class="input-group-text" style="width: 180px">Radio model</span>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="hamlib_deviceid"
placeholder="Search radio..."
style="width: 7rem"
@change="saveSettings"
v-model="settings.hamlib_deviceid"
/>
<datalist id="datalistOptions">
@change="onChange"
v-model.number="settings.remote.RADIO.model_id"
>
<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>
<option value="29001">ADAT www.adat.ch (29001)</option>
<option value="25016">AE9RB Si570 (25016)</option>
<option value="25017">AE9RB Si570 (25017)</option>
<option value="17001">Alinco DX-77 (17001)</option>
<option value="17002">Alinco DX-SR8 (17002)</option>
<option value="25006">AmQRP DDS-60 (25006)</option>
<option value="25013">AMSAT-UK FUNcube (25013)</option>
<option value="25018">AMSAT-UK FUNcube (25018)</option>
<option value="5008">AOR AR2700 (5008)</option>
<option value="5006">AOR AR3000A (5006)</option>
<option value="5005">AOR AR3030 (5005)</option>
<option value="5004">AOR AR5000 (5004)</option>
<option value="5014">AOR AR5000A (5014)</option>
<option value="5003">AOR AR7030 (5003)</option>
<option value="5015">AOR AR7030 (5015)</option>
<option value="5002">AOR AR8000 (5002)</option>
<option value="5001">AOR AR8200 (5001)</option>
<option value="5013">AOR AR8600 (5013)</option>
<option value="5016">AOR SR2200 (5016)</option>
<option value="32001">Barrett 2050 (32001)</option>
<option value="32003">Barrett 4050 (32003)</option>
<option value="32002">Barrett 950 (32002)</option>
<option value="34001">CODAN Envoy (34001)</option>
<option value="34002">CODAN NGT (34002)</option>
<option value="25003">Coding Technologies (25003)</option>
<option value="31002">Dorji DRA818U (31002)</option>
<option value="31001">Dorji DRA818V (31001)</option>
<option value="9002">Drake R-8A (9002)</option>
<option value="9003">Drake R-8B (9003)</option>
<option value="23003">DTTS Microwave (23003)</option>
<option value="23004">DTTS Microwave (23004)</option>
<option value="33001">ELAD FDM-DUO (33001)</option>
<option value="2021">Elecraft K2 (2021)</option>
<option value="2029">Elecraft K3 (2029)</option>
<option value="2043">Elecraft K3S (2043)</option>
<option value="2047">Elecraft K4 (2047)</option>
<option value="2044">Elecraft KX2 (2044)</option>
<option value="2045">Elecraft KX3 (2045)</option>
<option value="2038">Elecraft XG3 (2038)</option>
<option value="25001">Elektor Elektor (25001)</option>
<option value="25007">Elektor Elektor (25007)</option>
<option value="25012">FiFi FiFi-SDR (25012)</option>
<option value="2036">FlexRadio 6xxx (2036)</option>
<option value="23001">Flex-radio SDR-1000 (23001)</option>
<option value="2048">FlexRadio/ANAN PowerSDR/Thetis (2048)</option>
<option value="25015">Funkamateur FA-SDR (25015)</option>
<option value="35001">GOMSPACE GS100 (35001)</option>
<option value="2046">Hilberling PT-8000A (2046)</option>
<option value="25019">HobbyPCB RS-HFIQ (25019)</option>
<option value="3054">Icom IC (3054)</option>
<option value="3002">Icom IC-1275 (3002)</option>
<option value="3003">Icom IC-271 (3003)</option>
<option value="3072">Icom IC-2730 (3072)</option>
<option value="3004">Icom IC-275 (3004)</option>
<option value="3005">Icom IC-375 (3005)</option>
<option value="3006">Icom IC-471 (3006)</option>
<option value="3007">Icom IC-475 (3007)</option>
<option value="3008">Icom IC-575 (3008)</option>
<option value="3060">Icom IC-7000 (3060)</option>
<option value="3055">Icom IC-703 (3055)</option>
<option value="3085">Icom IC-705 (3085)</option>
<option value="3009">Icom IC-706 (3009)</option>
<option value="3010">Icom IC-706MkII (3010)</option>
<option value="3011">Icom IC-706MkIIG (3011)</option>
<option value="3012">Icom IC-707 (3012)</option>
<option value="3070">Icom IC-7100 (3070)</option>
<option value="3013">Icom IC-718 (3013)</option>
<option value="3061">Icom IC-7200 (3061)</option>
<option value="3014">Icom IC-725 (3014)</option>
<option value="3015">Icom IC-726 (3015)</option>
<option value="3016">Icom IC-728 (3016)</option>
<option value="3017">Icom IC-729 (3017)</option>
<option value="3073">Icom IC-7300 (3073)</option>
<option value="3019">Icom IC-735 (3019)</option>
<option value="3020">Icom IC-736 (3020)</option>
<option value="3021">Icom IC-737 (3021)</option>
<option value="3022">Icom IC-738 (3022)</option>
<option value="3067">Icom IC-7410 (3067)</option>
<option value="3023">Icom IC-746 (3023)</option>
<option value="3046">Icom IC-746PRO (3046)</option>
<option value="3024">Icom IC-751 (3024)</option>
<option value="3026">Icom IC-756 (3026)</option>
<option value="3027">Icom IC-756PRO (3027)</option>
<option value="3047">Icom IC-756PROII (3047)</option>
<option value="3057">Icom IC-756PROIII (3057)</option>
<option value="3063">Icom IC-7600 (3063)</option>
<option value="3028">Icom IC-761 (3028)</option>
<option value="3078">Icom IC-7610 (3078)</option>
<option value="3029">Icom IC-765 (3029)</option>
<option value="3062">Icom IC-7700 (3062)</option>
<option value="3030">Icom IC-775 (3030)</option>
<option value="3045">Icom IC-78 (3045)</option>
<option value="3056">Icom IC-7800 (3056)</option>
<option value="3031">Icom IC-781 (3031)</option>
<option value="3075">Icom IC-7850/7851 (3075)</option>
<option value="3032">Icom IC-820H (3032)</option>
<option value="3034">Icom IC-821H (3034)</option>
<option value="3044">Icom IC-910 (3044)</option>
<option value="3068">Icom IC-9100 (3068)</option>
<option value="3065">Icom IC-92D (3065)</option>
<option value="3035">Icom IC-970 (3035)</option>
<option value="3081">Icom IC-9700 (3081)</option>
<option value="3086">Icom IC-F8101 (3086)</option>
<option value="30001">Icom IC-M700PRO (30001)</option>
<option value="30003">Icom IC-M710 (30003)</option>
<option value="30002">Icom IC-M802 (30002)</option>
<option value="30004">Icom IC-M803 (30004)</option>
<option value="4002">Icom IC-PCR100 (4002)</option>
<option value="4001">Icom IC-PCR1000 (4001)</option>
<option value="4003">Icom IC-PCR1500 (4003)</option>
<option value="4004">Icom IC-PCR2500 (4004)</option>
<option value="3036">Icom IC-R10 (3036)</option>
<option value="3058">Icom IC-R20 (3058)</option>
<option value="3080">Icom IC-R30 (3080)</option>
<option value="3077">Icom IC-R6 (3077)</option>
<option value="3040">Icom IC-R7000 (3040)</option>
<option value="3037">Icom IC-R71 (3037)</option>
<option value="3041">Icom IC-R7100 (3041)</option>
<option value="3038">Icom IC-R72 (3038)</option>
<option value="3039">Icom IC-R75 (3039)</option>
<option value="3042">Icom ICR-8500 (3042)</option>
<option value="3079">Icom IC-R8600 (3079)</option>
<option value="3043">Icom IC-R9000 (3043)</option>
<option value="3066">Icom IC-R9500 (3066)</option>
<option value="3069">Icom IC-RX7 (3069)</option>
<option value="3083">Icom ID-31 (3083)</option>
<option value="3082">Icom ID-4100 (3082)</option>
<option value="3084">Icom ID-51 (3084)</option>
<option value="3071">Icom ID-5100 (3071)</option>
<option value="6001">JRC JST-145 (6001)</option>
<option value="6002">JRC JST-245 (6002)</option>
<option value="6005">JRC NRD-525 (6005)</option>
<option value="6006">JRC NRD-535D (6006)</option>
<option value="6007">JRC NRD-545 (6007)</option>
<option value="18001">Kachina 505DSP (18001)</option>
<option value="2015">Kenwood R-5000 (2015)</option>
<option value="2033">Kenwood TH-D72A (2033)</option>
<option value="2042">Kenwood TH-D74 (2042)</option>
<option value="2017">Kenwood TH-D7A (2017)</option>
<option value="2019">Kenwood TH-F6A (2019)</option>
<option value="2020">Kenwood TH-F7E (2020)</option>
<option value="2023">Kenwood TH-G71 (2023)</option>
<option value="2026">Kenwood TM-D700 (2026)</option>
<option value="2034">Kenwood TM-D710(G) (2034)</option>
<option value="2027">Kenwood TM-V7 (2027)</option>
<option value="2035">Kenwood TM-V71(A) (2035)</option>
<option value="2030">Kenwood TRC-80 (2030)</option>
<option value="2025">Kenwood TS-140S (2025)</option>
<option value="2014">Kenwood TS-2000 (2014)</option>
<option value="2002">Kenwood TS-440S (2002)</option>
<option value="2003">Kenwood TS-450S (2003)</option>
<option value="2028">Kenwood TS-480 (2028)</option>
<option value="2001">Kenwood TS-50S (2001)</option>
<option value="2004">Kenwood TS-570D (2004)</option>
<option value="2016">Kenwood TS-570S (2016)</option>
<option value="2031">Kenwood TS-590S (2031)</option>
<option value="2037">Kenwood TS-590SG (2037)</option>
<option value="2024">Kenwood TS-680S (2024)</option>
<option value="2005">Kenwood TS-690S (2005)</option>
<option value="2006">Kenwood TS-711 (2006)</option>
<option value="2007">Kenwood TS-790 (2007)</option>
<option value="2008">Kenwood TS-811 (2008)</option>
<option value="2009">Kenwood TS-850 (2009)</option>
<option value="2010">Kenwood TS-870S (2010)</option>
<option value="2041">Kenwood TS-890S (2041)</option>
<option value="2022">Kenwood TS-930 (2022)</option>
<option value="2011">Kenwood TS-940S (2011)</option>
<option value="2012">Kenwood TS-950S (2012)</option>
<option value="2013">Kenwood TS-950SDX (2013)</option>
<option value="2039">Kenwood TS-990S (2039)</option>
<option value="25011">KTH-SDR kit (25011)</option>
<option value="2050">Lab599 TX-500 (2050)</option>
<option value="10004">Lowe HF-235 (10004)</option>
<option value="1045">M0NKA mcHF (1045)</option>
<option value="2049">Malachite DSP (2049)</option>
<option value="3074">Microtelecom Perseus (3074)</option>
<option value="25008">mRS miniVNA (25008)</option>
<option value="25014">N2ADR HiQSDR (25014)</option>
<option value="2040">OpenHPSDR PiHPSDR (2040)</option>
<option value="3053">Optoelectronics OptoScan456 (3053)</option>
<option value="3052">Optoelectronics OptoScan535 (3052)</option>
<option value="28001">Philips/Simoco PRM8060 (28001)</option>
<option value="11005">Racal RA3702 (11005)</option>
<option value="11003">Racal RA6790/GM (11003)</option>
<option value="8004">Radio Shack (8004)</option>
<option value="24001">RFT EKD-500 (24001)</option>
<option value="27002">Rohde&Schwarz EB200 (27002)</option>
<option value="27004">Rohde&Schwarz EK895/6 (27004)</option>
<option value="27001">Rohde&Schwarz ESMC (27001)</option>
<option value="27003">Rohde&Schwarz XK2100 (27003)</option>
<option value="25002">SAT-Schneider DRT1 (25002)</option>
<option value="2051">SDRPlay SDRUno (2051)</option>
<option value="2032">SigFox Transfox (2032)</option>
<option value="14004">Skanti TRP (14004)</option>
<option value="14002">Skanti TRP8000 (14002)</option>
<option value="25009">SoftRock Si570 (25009)</option>
<option value="22001">TAPR DSP-10 (22001)</option>
<option value="3064">Ten-Tec Delta (3064)</option>
<option value="3051">Ten-Tec Omni (3051)</option>
<option value="16003">Ten-Tec RX-320 (16003)</option>
<option value="16012">Ten-Tec RX-331 (16012)</option>
<option value="16004">Ten-Tec RX-340 (16004)</option>
<option value="16005">Ten-Tec RX-350 (16005)</option>
<option value="16007">Ten-Tec TT-516 (16007)</option>
<option value="16002">Ten-Tec TT-538 (16002)</option>
<option value="16001">Ten-Tec TT-550 (16001)</option>
<option value="16008">Ten-Tec TT-565 (16008)</option>
<option value="16009">Ten-Tec TT-585 (16009)</option>
<option value="16011">Ten-Tec TT-588 (16011)</option>
<option value="16013">Ten-Tec TT-599 (16013)</option>
<option value="8002">Uniden BC245xlt (8002)</option>
<option value="8006">Uniden BC250D (8006)</option>
<option value="8001">Uniden BC780xlt (8001)</option>
<option value="8003">Uniden BC895xlt (8003)</option>
<option value="8012">Uniden BC898T (8012)</option>
<option value="8010">Uniden BCD-396T (8010)</option>
<option value="8011">Uniden BCD-996T (8011)</option>
<option value="1033">Vertex Standard (1033)</option>
<option value="26001">Video4Linux SW/FM (26001)</option>
<option value="26002">Video4Linux2 SW/FM (26002)</option>
<option value="12004">Watkins-Johnson WJ-8888 (12004)</option>
<option value="15001">Winradio WR-1000 (15001)</option>
<option value="15002">Winradio WR-1500 (15002)</option>
<option value="15003">Winradio WR-1550 (15003)</option>
<option value="15004">Winradio WR-3100 (15004)</option>
<option value="15005">Winradio WR-3150 (15005)</option>
<option value="15006">Winradio WR-3500 (15006)</option>
<option value="15007">Winradio WR-3700 (15007)</option>
<option value="15009">Winradio WR-G313 (15009)</option>
<option value="3088">Xiegu G90 (3088)</option>
<option value="3076">Xiegu X108G (3076)</option>
<option value="3089">Xiegu X5105 (3089)</option>
<option value="3087">Xiegu X6100 (3087)</option>
<option value="1017">Yaesu FRG-100 (1017)</option>
<option value="1019">Yaesu FRG-8800 (1019)</option>
<option value="1018">Yaesu FRG-9600 (1018)</option>
<option value="1021">Yaesu FT-100 (1021)</option>
<option value="1003">Yaesu FT-1000D (1003)</option>
<option value="1024">Yaesu FT-1000MP (1024)</option>
<option value="1029">Yaesu FT-2000 (1029)</option>
<option value="1027">Yaesu FT-450 (1027)</option>
<option value="1046">Yaesu FT-450D (1046)</option>
<option value="1039">Yaesu FT-600 (1039)</option>
<option value="1047">Yaesu FT-650 (1047)</option>
<option value="1049">Yaesu FT-710 (1049)</option>
<option value="1010">Yaesu FT-736R (1010)</option>
<option value="1005">Yaesu FT-747GX (1005)</option>
<option value="1006">Yaesu FT-757GX (1006)</option>
<option value="1007">Yaesu FT-757GXII (1007)</option>
<option value="1009">Yaesu FT-767GX (1009)</option>
<option value="1020">Yaesu FT-817 (1020)</option>
<option value="1041">Yaesu FT-818 (1041)</option>
<option value="1011">Yaesu FT-840 (1011)</option>
<option value="1001">Yaesu FT-847 (1001)</option>
<option value="1038">Yaesu FT-847UNI (1038)</option>
<option value="1022">Yaesu FT-857 (1022)</option>
<option value="1015">Yaesu FT-890 (1015)</option>
<option value="1036">Yaesu FT-891 (1036)</option>
<option value="1023">Yaesu FT-897 (1023)</option>
<option value="1043">Yaesu FT-897D (1043)</option>
<option value="1013">Yaesu FT-900 (1013)</option>
<option value="1014">Yaesu FT-920 (1014)</option>
<option value="1028">Yaesu FT-950 (1028)</option>
<option value="1031">Yaesu FT-980 (1031)</option>
<option value="1016">Yaesu FT-990 (1016)</option>
<option value="1048">Yaesu FT-990 (1048)</option>
<option value="1035">Yaesu FT-991 (1035)</option>
<option value="1042">Yaesu FTDX-10 (1042)</option>
<option value="1040">Yaesu FTDX-101D (1040)</option>
<option value="1044">Yaesu FTDX-101MP (1044)</option>
<option value="1034">Yaesu FTDX-1200 (1034)</option>
<option value="1037">Yaesu FTDX-3000 (1037)</option>
<option value="1032">Yaesu FTDX-5000 (1032)</option>
<option value="1030">Yaesu FTDX-9000 (1030)</option>
<option value="1004">Yaesu MARK-V (1004)</option>
<option value="1025">Yaesu MARK-V (1025)</option>
<option value="1026">Yaesu VR-5000 (1026)</option>
</select>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">Radio port</span>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="hamlib_deviceport"
style="width: 7rem"
@change="onChange"
v-model="settings.remote.RADIO.serial_port"
>
<option
v-for="option in serialDeviceOptions()"
v-bind:value="option.port"
>
{{ option.description }}
</option>
</select>
</div>
<div class="input-group input-group-sm mb-1">
@ -355,8 +354,8 @@ function saveSettings() {
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="hamlib_serialspeed"
@change="saveSettings"
v-model="settings.hamlib_serialspeed"
@change="onChange"
v-model.number="settings.remote.RADIO.serial_speed"
>
<option selected value="ignore">-- ignore --</option>
<option value="1200">1200</option>
@ -378,8 +377,8 @@ function saveSettings() {
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="hamlib_data_bits"
@change="saveSettings"
v-model="settings.hamlib_data_bits"
@change="onChange"
v-model.number="settings.remote.RADIO.data_bits"
>
<option selected value="ignore">-- ignore --</option>
<option value="7">7</option>
@ -393,8 +392,8 @@ function saveSettings() {
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="hamlib_stop_bits"
@change="saveSettings"
v-model="settings.hamlib_stop_bits"
@change="onChange"
v-model.number="settings.remote.RADIO.stop_bits"
>
<option selected value="ignore">-- ignore --</option>
<option value="1">1</option>
@ -408,8 +407,8 @@ function saveSettings() {
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="hamlib_handshake"
@change="saveSettings"
v-model="settings.hamlib_handshake"
@change="onChange"
v-model="settings.remote.RADIO.serial_handshake"
>
<option selected value="ignore">-- ignore --</option>
<option value="None">None (Default)</option>
@ -422,10 +421,15 @@ function saveSettings() {
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="hamlib_ptt_port"
@change="saveSettings"
v-model="settings.hamlib_ptt_port"
@change="onChange"
v-model="settings.remote.RADIO.ptt_port"
>
<option selected value="ignore">-- ignore --</option>
<option
v-for="option in serialDeviceOptions()"
v-bind:value="option.port"
>
{{ option.description }}
</option>
</select>
</div>
<div class="input-group input-group-sm mb-1">
@ -436,8 +440,8 @@ function saveSettings() {
aria-label=".form-select-sm"
id="hamlib_pttprotocol"
style="width: 0.5rem"
@change="saveSettings"
v-model="settings.hamlib_pttprotocol"
@change="onChange"
v-model="settings.remote.RADIO.ptt_type"
>
<option selected value="ignore">-- ignore --</option>
<option value="NONE">NONE</option>
@ -457,8 +461,8 @@ function saveSettings() {
aria-label=".form-select-sm"
id="hamlib_dcd"
style="width: 0.5rem"
@change="saveSettings"
v-model="settings.hamlib_dcd"
@change="onChange"
v-model="settings.remote.RADIO.serial_dcd"
>
<option selected value="ignore">-- ignore --</option>
<option value="NONE">NONE</option>
@ -477,8 +481,8 @@ function saveSettings() {
aria-label=".form-select-sm"
id="hamlib_dtrstate"
style="width: 0.5rem"
@change="saveSettings"
v-model="settings.hamlib_dtrstate"
@change="onChange"
v-model="settings.remote.RADIO.serial_dtr"
>
<option selected value="ignore">-- ignore --</option>
<option value="OFF">OFF</option>
@ -515,8 +519,8 @@ function saveSettings() {
id="hamlib_rigctld_custom_args"
aria-label="Custom arguments"
aria-describedby="basic-addon1"
@change="saveSettings"
v-model="settings.hamlib_rigctld_custom_args"
@change="onChange"
v-model="settings.remote.RIGCTLD.arguments"
/>
</div>
</template>

View file

@ -1,53 +1,141 @@
<script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler";
import { setActivePinia } from "pinia";
import { settingsStore as settings, onChange } from "../store/settingsStore.js";
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 saveSettings() {
saveSettingsToFile();
}
import { startModem, stopModem } from "../js/api.js";
import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
</script>
<template>
<div>
<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="startModem"
v-bind:class="{ disabled: state.is_modem_running === true }"
>
<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="stopModem"
v-bind:class="{ disabled: state.is_modem_running === false }"
>
<i class="bi bi-stop-fill"></i>
<span class="ms-2">stop modem</span>
</button>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">modem port</span>
<span class="input-group-text" style="width: 180px">Modem port</span>
<input
type="text"
type="number"
class="form-control"
placeholder="modem port"
id="modem_port"
maxlength="5"
max="65534"
min="1025"
@change="saveSettings"
v-model="settings.modem_port"
v-model.number="settings.local.port"
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">modem host</span>
<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"
v-model="settings.local.host"
/>
</div>
<!-- Audio Input Device -->
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Audio Input device</label>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
@change="onChange"
v-model="settings.remote.AUDIO.input_device"
>
<option v-for="option in audioInputOptions()" v-bind:value="option.id">
{{ option.name }} [{{ option.api }}]
</option>
</select>
</div>
<!-- Audio Output Device -->
<div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Audio Output device</label>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
@change="onChange"
v-model="settings.remote.AUDIO.output_device"
>
<option v-for="option in audioOutputOptions()" v-bind:value="option.id">
{{ option.name }} [{{ option.api }}]
</option>
</select>
</div>
<!-- Audio rx level-->
<div class="input-group input-group-sm mb-1">
<span class="input-group-text w-25">RX Audio Level</span>
<span class="input-group-text w-25">{{
settings.remote.AUDIO.rx_audio_level
}}</span>
<span class="input-group-text w-50">
<input
type="range"
class="form-range"
min="-30"
max="20"
step="1"
id="audioLevelRX"
@change="onChange"
v-model.number="settings.remote.AUDIO.rx_audio_level"
/></span>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text w-25">TX Audio Level</span>
<span class="input-group-text w-25">{{
settings.remote.AUDIO.tx_audio_level
}}</span>
<span class="input-group-text w-50">
<input
type="range"
class="form-range"
min="-30"
max="20"
step="1"
id="audioLevelTX"
@change="onChange"
v-model.number="settings.remote.AUDIO.tx_audio_level"
/></span>
</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"
@change="onChange"
v-model.number="settings.remote.MODEM.tx_delay"
>
<option value="0">0</option>
<option value="50">50</option>
@ -79,27 +167,27 @@ function saveSettings() {
<select
class="form-select form-select-sm"
id="tuning_range_fmin"
@change="saveSettings"
v-model="settings.tuning_range_fmin"
@change="onChange"
v-model.number="settings.remote.MODEM.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>
<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>
</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"
@change="onChange"
v-model.number="settings.remote.MODEM.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>
<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>
</select>
</div>
<div class="input-group input-group-sm mb-1">
@ -109,8 +197,8 @@ function saveSettings() {
aria-label=".form-select-sm"
id="beaconInterval"
style="width: 6rem"
@change="saveSettings"
v-model="settings.beacon_interval"
@change="onChange"
v-model.number="settings.remote.MODEM.beacon_interval"
>
<option value="60">60 secs</option>
<option value="90">90 secs</option>
@ -122,40 +210,7 @@ function saveSettings() {
<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">
@ -164,10 +219,8 @@ function saveSettings() {
class="form-check-input"
type="checkbox"
id="250HzModeSwitch"
v-model="settings.low_bandwidth_mode"
true-value="True"
false-value="False"
@change="saveSettings"
v-model="settings.remote.MODEM.enable_low_bandwidth_mode"
@change="onChange"
/>
<label class="form-check-label" for="250HzModeSwitch">250Hz</label>
</div>
@ -181,10 +234,8 @@ function saveSettings() {
class="form-check-input"
type="checkbox"
id="respondCQSwitch"
v-model="settings.respond_to_cq"
true-value="True"
false-value="False"
@change="saveSettings"
v-model="settings.remote.MODEM.respond_to_cq"
@change="onChange"
/>
<label class="form-check-label" for="respondCQSwitch">QRV</label>
</div>
@ -196,8 +247,8 @@ function saveSettings() {
<select
class="form-select form-select-sm"
id="rx_buffer_size"
@change="saveSettings"
v-model="settings.rx_buffer_size"
@change="onChange"
v-model.number="settings.remote.MODEM.rx_buffer_size"
>
<option value="1">1</option>
<option value="2">2</option>

View file

@ -0,0 +1,78 @@
<script setup lang="ts">
import { settingsStore as settings, onChange } from "../store/settingsStore.js";
import settings_hamlib from "./settings_hamlib.vue";
import settings_tci from "./settings_tci.vue";
</script>
<template>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">Rig Control</span>
<select
class="form-select form-select-sm"
aria-label=".form-select-sm"
id="rigcontrol_radiocontrol"
@change="onChange"
v-model="settings.remote.RADIO.control"
>
<option selected value="disabled">
Disabled / VOX (no rig control - use with VOX)
</option>
<option selected value="rigctld">Rigctld (Hamlib)</option>
<option selected value="tci">TCI</option>
</select>
</div>
<hr class="m-2" />
<nav>
<div class="nav nav-tabs" id="nav-tab" role="tablist">
<button
class="nav-link active"
id="nav-home-tab"
data-bs-toggle="tab"
data-bs-target="#nav-hamlib"
type="button"
role="tab"
aria-controls="nav-home"
aria-selected="true"
>
Hamlib
</button>
<button
class="nav-link"
id="nav-profile-tab"
data-bs-toggle="tab"
data-bs-target="#nav-tci"
type="button"
role="tab"
aria-controls="nav-profile"
aria-selected="false"
>
TCI
</button>
</div>
</nav>
<div class="tab-content" id="nav-tabContent">
<div
class="tab-pane fade show active"
id="nav-hamlib"
role="tabpanel"
aria-labelledby="nav-hamlib-tab"
tabindex="0"
>
<settings_hamlib />
</div>
<div
class="tab-pane fade"
id="nav-tci"
role="tabpanel"
aria-labelledby="nav-tci-tab"
tabindex="0"
>
<settings_tci />
</div>
</div>
<hr class="m-2" />
</template>

View file

@ -0,0 +1,80 @@
<script setup lang="ts">
import {
settingsStore as settings,
onChange,
getRemote,
} from "../store/settingsStore.js";
import {
validateCallsignWithSSID,
validateCallsignWithoutSSID,
} from "../js/freedata";
function validateCall() {
if (validateCallsignWithoutSSID(settings.remote.STATION.mycall))
//Send new callsign to modem if valid
onChange();
//Reload settings from modem as invalid callsign was passed in
else getRemote();
}
</script>
<template>
<!-- station callsign -->
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px"
>Your station callsign</span
>
<input
type="text"
class="form-control"
placeholder="Enter your callsign and save it"
id="myCall"
aria-label="Station Callsign"
aria-describedby="basic-addon1"
v-model="settings.remote.STATION.mycall"
@change="validateCall"
/>
</div>
<!-- station ssid -->
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">Call SSID</span>
<select
class="form-select form-select-sm w-50"
id="myCallSSID"
@change="onChange"
v-model.number="settings.remote.STATION.myssid"
>
<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>
<!-- station grid locator -->
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">Grid Locator</span>
<input
type="text"
class="form-control"
placeholder="Your grid locator"
id="myGrid"
maxlength="6"
aria-label="Station Grid Locator"
aria-describedby="basic-addon1"
@change="onChange"
v-model="settings.remote.STATION.mygrid"
/>
</div>
</template>

View file

@ -0,0 +1,34 @@
<script setup lang="ts">
import { settingsStore as settings, onChange } from "../store/settingsStore.js";
import { serialDeviceOptions } from "../js/deviceFormHelper";
</script>
<template>
<hr class="m-2" />
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">TCI IP Address</span>
<input
type="text"
class="form-control"
placeholder="TCI IP"
id="rigcontrol_tci_ip"
aria-label="Device IP"
@change="onChange"
v-model="settings.remote.TCI.tci_ip"
/>
</div>
<div class="input-group input-group-sm mb-1">
<span class="input-group-text" style="width: 180px">TCI port</span>
<input
type="text"
class="form-control"
placeholder="TCI port"
id="rigcontrol_tci_port"
aria-label="Device Port"
@change="onChange"
v-model="settings.remote.TCI.tci_port"
/>
</div>
</template>

View file

@ -1,16 +1,11 @@
<script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler";
import { setConfig } from "../js/api";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
function saveSettings() {
saveSettingsToFile();
}
import { settingsStore as settings, onChange } from "../store/settingsStore.js";
</script>
<template>
@ -22,10 +17,8 @@ function saveSettings() {
class="form-check-input"
type="checkbox"
id="ExplorerSwitch"
@change="saveSettings"
v-model="settings.enable_explorer"
true-value="True"
false-value="False"
@change="onChange"
v-model="settings.remote.STATION.enable_explorer"
/>
<label class="form-check-label" for="ExplorerSwitch">Publish</label>
</div>
@ -39,10 +32,8 @@ function saveSettings() {
class="form-check-input"
type="checkbox"
id="ExplorerStatsSwitch"
@change="saveSettings"
v-model="settings.explorer_stats"
true-value="True"
false-value="False"
@change="onChange"
v-model="settings.remote.STATION.enable_stats"
/>
<label class="form-check-label" for="ExplorerStatsSwitch"
>Publish stats</label

129
gui/src/js/api.js Normal file
View file

@ -0,0 +1,129 @@
import { settingsStore as settings } from "../store/settingsStore.js";
import {
validateCallsignWithSSID,
validateCallsignWithoutSSID,
} from "./freedata";
function buildURL(params, endpoint) {
const url = "http://" + params.host + ":" + params.port + endpoint;
return url;
}
async function apiGet(endpoint) {
try {
const response = await fetch(buildURL(settings.local, endpoint));
if (!response.ok) {
throw new Error(`REST response not ok: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error getting from REST:", error);
}
}
export async function apiPost(endpoint, payload = {}) {
try {
const response = await fetch(buildURL(settings.local, endpoint), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`REST response not ok: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error posting to REST:", error);
}
}
export async function getVersion() {
let data = await apiGet("/version").then((res) => {
return res;
});
return data.version;
//return data["version"];
}
export async function getConfig() {
return apiGet("/config");
}
export function setConfig(config) {
return apiPost("/config", config);
}
export function getAudioDevices() {
return apiGet("/devices/audio");
}
export function getSerialDevices() {
return apiGet("/devices/serial");
}
export function setModemBeacon(enabled = false) {
return apiPost("/modem/beacon", { enabled: enabled });
}
export function sendModemCQ() {
return apiPost("/modem/cqcqcq");
}
export function sendModemPing(dxcall) {
if (
validateCallsignWithSSID(dxcall) === false &&
validateCallsignWithoutSSID(dxcall) === true
) {
dxcall = String(dxcall).toUpperCase().trim();
dxcall = dxcall + "-0";
}
dxcall = String(dxcall).toUpperCase().trim();
if (validateCallsignWithSSID(dxcall))
return apiPost("/modem/ping_ping", { dxcall: dxcall });
}
export function sendModemARQRaw(mycall, dxcall, data, uuid) {
return apiPost("/modem/send_arq_raw", {
mycallsign: mycall,
dxcall: dxcall,
data: data,
uuid: uuid,
});
}
export function stopTransmission() {
return apiPost("/modem/stop_transmission");
}
export function sendModemTestFrame() {
return apiPost("/modem/send_test_frame");
}
export function startModem() {
return apiPost("/modem/start");
}
export function stopModem() {
return apiPost("/modem/stop");
}
export function getModemState() {
return apiGet("/modem/state");
}
export function setRadioParameters(frequency, mode, rf_level) {
return apiPost("/radio", {
radio_frequency: frequency,
radio_mode: mode,
radio_rf_level: rf_level,
});
}
export function getRadioStatus() {
return apiGet("/radio");
}

View file

@ -13,14 +13,21 @@ const chat = useChatStore(pinia);
import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { settingsStore as settings } from "../store/settingsStore.js";
import { sendMessage, sendBroadcastChannel } from "./sock.js";
import { displayToast } from "./popupHandler.js";
//const FD = require("./src/js/freedata.js");
import { btoa_FD } from "./freedata.js";
import {
atob_FD,
btoa_FD,
sortByProperty,
sortByPropertyDesc,
} from "./freedata.js";
import { sendModemARQRaw } from "../js/api.js";
const split_char = "0;1;";
// define default message object
interface Attachment {
@ -67,6 +74,13 @@ interface beaconDefaultObject {
snr: string;
}
interface newChatDefaultObject {
command: string;
is_new: boolean;
timestamp: number;
dxcallsign: string;
}
// ---- MessageDB
try {
var PouchDB = require("pouchdb");
@ -179,7 +193,7 @@ export function newBroadcast(broadcastChannel, chatmessage) {
};
//sendMessage(newChatObj)
sendBroadcastChannel(newChatObj);
//sendBroadcastChannel(newChatObj);
addObjToDatabase(newChatObj);
}
@ -279,16 +293,6 @@ function sortChatList() {
return reorderedData;
}
//https://medium.com/@asadise/sorting-a-json-array-according-one-property-in-javascript-18b1d22cd9e9
function sortByProperty(property) {
return function (a, b) {
if (a[property] > b[property]) return 1;
else if (a[property] < b[property]) return -1;
return 0;
};
}
export function getMessageAttachment(id) {
return new Promise(async (resolve, reject) => {
try {
@ -395,11 +399,11 @@ async function dbClean() {
//Too slow on older/slower machines
//await db.compact();
let message = "Database maintenance is complete";
displayToast("info", "bi bi-info-circle", message, 5000);
let message = "Database maintenance is complete, ";
//displayToast("info", "bi bi-info-circle", message, 5000);
message = "Removed " + itemCount + " items from database";
displayToast("info", "bi bi-info-circle", message, 5000);
message += "removed " + itemCount + " items from database";
console.log(message);
}
// function to update transmission status
@ -419,7 +423,7 @@ export function updateUnsortedChatListEntry(uuid, object, value) {
var data = getFromUnsortedChatListByUUID(uuid);
if (data) {
data[object] = value;
console.log("Entry updated:", data[object]);
//console.log("Entry updated:", data[object]);
chat.sorted_chat_list = sortChatList();
return data;
}
@ -435,7 +439,7 @@ export function updateUnsortedChatListEntry(uuid, object, value) {
}
*/
console.log("Entry not updated:", object);
//console.log("Entry not updated:", object);
return null; // Return null if not found
}
@ -452,22 +456,27 @@ export function getNewMessagesByDXCallsign(dxcallsign): [number, number, any] {
let new_counter = 0;
let total_counter = 0;
let item_array = [];
if (
typeof dxcallsign !== "undefined" &&
typeof chat.sorted_chat_list[dxcallsign] !== "undefined"
) {
for (const key in chat.sorted_chat_list[dxcallsign]) {
//console.log(chat.sorted_chat_list[dxcallsign][key])
//item_array.push(chat.sorted_chat_list[dxcallsign][key])
if (chat.sorted_chat_list[dxcallsign][key].is_new) {
item_array.push(chat.sorted_chat_list[dxcallsign][key]);
new_counter += 1;
try {
if (
typeof dxcallsign !== "undefined" &&
typeof chat.sorted_chat_list[dxcallsign] !== "undefined"
) {
for (const key in chat.sorted_chat_list[dxcallsign]) {
//console.log(chat.sorted_chat_list[dxcallsign][key])
//item_array.push(chat.sorted_chat_list[dxcallsign][key])
if (chat.sorted_chat_list[dxcallsign][key].is_new) {
item_array.push(chat.sorted_chat_list[dxcallsign][key]);
new_counter += 1;
}
total_counter += 1;
}
total_counter += 1;
}
}
return [total_counter, new_counter, item_array];
return [total_counter, new_counter, item_array];
} catch (e) {
console.log(e);
return [0, 0, item_array];
}
}
export function resetIsNewMessage(uuid, value) {
@ -485,7 +494,7 @@ export function databaseUpsert(id, object, value) {
})
.then(function (res) {
// success, res is {rev: '1-xxx', updated: true, id: 'myDocId'}
console.log(res);
//console.log(res);
})
.catch(function (err) {
// error
@ -592,7 +601,7 @@ function addObjToDatabase(newobj) {
console.log("new database entry");
console.log(response);
if (newobj.command === "msg") {
if (newobj.command === "msg" || newobj.command === "newchat") {
chat.unsorted_chat_list.push(newobj);
chat.sorted_chat_list = sortChatList();
}
@ -692,6 +701,31 @@ function deleteFromDatabaseByCallsign(callsign) {
console.log(err);
});
}
//Function creates a new 'newchat' database entry when user initates a new chat, otherwise cannot send messages unless receiving a message/beacon from user first
/**
* Add a newuser to the database, for when newuser button is clicked
* @param {string} call callsign of new user
*/
export function startChatWithNewStation(call) {
let newchat: newChatDefaultObject = {
command: "newchat",
is_new: false,
timestamp: Math.floor(new Date().getTime() / 1000),
dxcallsign: call,
};
addObjToDatabase(newchat);
if (!chat.sorted_beacon_list[call]) {
// If not, initialize it with an empty array for snr values
chat.sorted_beacon_list[call] = {
call,
snr: [],
timestamp: [],
};
chat.callsign_list.add(call);
}
//chat.unsorted_chat_list.push(newchat);
//chat.sorted_chat_list = sortChatList();
}
// function for handling a received beacon
export function newBeaconReceived(obj) {
@ -746,11 +780,11 @@ export function newBeaconReceived(obj) {
});
// check if auto retry enabled
console.log("-----------------------------------------");
console.log(settings.enable_auto_retry.toUpperCase());
if (settings.enable_auto_retry.toUpperCase() == "TRUE") {
checkForWaitingMessages(dxcallsign);
}
//console.log("-----------------------------------------");
//console.log(settings.enable_auto_retry.toUpperCase());
//if (settings.enable_auto_retry.toUpperCase() == "TRUE") {
// checkForWaitingMessages(dxcallsign);
//}
}
// function for handling a received message
@ -830,27 +864,39 @@ export function newMessageReceived(message, protocol) {
*/
console.log(protocol);
var encoded_data = atob_FD(message);
var splitted_data = encoded_data.split(split_char);
// new message received
if (splitted_data[0] == "m") {
console.log(splitted_data);
message = splitted_data;
} else {
return;
}
let newChatObj: messageDefaultObject = {
command: "msg",
hmac_signed: protocol["hmac_signed"],
hmac_signed: false,
percent: 100,
bytesperminute: protocol["bytesperminute"],
bytesperminute: 0,
is_new: true,
_id: message[3],
timestamp: message[4],
dxcallsign: protocol["dxcallsign"],
dxgrid: protocol["dxgrid"],
dxcallsign: protocol["dxcall"],
dxgrid: "",
msg: message[5],
checksum: message[2],
type: protocol["status"],
status: protocol["status"],
type: "received",
status: "received",
attempt: 1,
uuid: message[3],
duration: protocol["duration"],
nacks: protocol["nacks"],
speed_list: protocol["speed_list"],
duration: 0,
nacks: 0,
speed_list: "[]",
_attachments: {
[message[6]]: {
content_type: message[7],
@ -1029,13 +1075,12 @@ async function checkForWaitingMessages(dxcall) {
// this ensures, we are only sending one message at once
// @ts-expect-error
console.log(result.docs[0]);
console.log(
"attempt: " +
// @ts-expect-error
result.docs[0].attempt +
"/" +
settings.max_retry_attempts,
);
//console.log(
// "attempt: " +
// result.docs[0].attempt +
// "/" +
// settings.max_retry_attempts,
//);
// @ts-expect-error
if (result.docs[0].attempt < settings.max_retry_attempts) {
console.log("repeating message...");
@ -1049,3 +1094,42 @@ async function checkForWaitingMessages(dxcall) {
console.log(err);
});
}
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;
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,
);
let mycallsign =
settings.remote.STATION.mycall + "-" + settings.remote.STATION.myssid;
sendModemARQRaw(mycallsign, dxcallsign, data, uuid);
}

View file

@ -1,281 +0,0 @@
//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 + "-" + settings.myssid,
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);
}

File diff suppressed because it is too large Load diff

View file

@ -1,186 +0,0 @@
const path = require("path");
const { ipcRenderer } = require("electron");
// https://stackoverflow.com/a/26227660
var appDataFolder =
process.env.APPDATA ||
(process.platform == "darwin"
? process.env.HOME + "/Library/Application Support"
: process.env.HOME + "/.config");
var configFolder = path.join(appDataFolder, "FreeDATA");
var configPath = path.join(configFolder, "config.json");
const config = require(configPath);
// WINDOW LISTENER
window.addEventListener("DOMContentLoaded", () => {
document
.getElementById("enable_filter_info")
.addEventListener("click", () => {
if (document.getElementById("enable_filter_info").checked) {
display_class("table-info", true);
} else {
display_class("table-info", false);
}
});
document
.getElementById("enable_filter_debug")
.addEventListener("click", () => {
if (document.getElementById("enable_filter_debug").checked) {
display_class("table-debug", true);
} else {
display_class("table-debug", false);
}
});
document
.getElementById("enable_filter_warning")
.addEventListener("click", () => {
if (document.getElementById("enable_filter_warning").checked) {
display_class("table-warning", true);
} else {
display_class("table-warning", false);
}
});
document
.getElementById("enable_filter_error")
.addEventListener("click", () => {
if (document.getElementById("enable_filter_error").checked) {
display_class("table-danger", true);
} else {
display_class("table-danger", false);
}
});
});
function display_class(class_name, state) {
var collection = document.getElementsByClassName(class_name);
console.log(collection);
for (let i = 0; i < collection.length; i++) {
if (state == true) {
collection[i].style.display = "table-row";
} else {
collection[i].style.display = "None";
}
}
}
ipcRenderer.on("action-update-log", (event, arg) => {
var entry = arg.entry;
// remove ANSI characters from string, caused by color logging
// https://stackoverflow.com/a/29497680
entry = entry.replace(
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
"",
);
var tbl = document.getElementById("log");
var row = document.createElement("tr");
var timestamp = document.createElement("td");
var timestampText = document.createElement("span");
//datetime = new Date();
//timestampText.innerText = datetime.toISOString();
timestampText.innerText = entry.slice(0, 19);
timestamp.appendChild(timestampText);
var type = document.createElement("td");
var typeText = document.createElement("span");
// typeText.innerText = entry.slice(10, 30).match(/[\[](.*)[^\]]/g);
console.log(entry.match(/\[[^\]]+\]/g));
try {
typeText.innerText = entry.match(/\[[^\]]+\]/g)[0];
} catch (e) {
typeText.innerText = "-";
}
// let res = str.match(/[\[](.*)[^\]]/g);
type.appendChild(typeText);
var area = document.createElement("td");
var areaText = document.createElement("span");
//areaText.innerText = entry.slice(10, 50).match(/[\] \[](.*)[^\]]/g);
//areaText.innerText = entry.match(/\[[^\]]+\]/g)[1];
try {
areaText.innerText = entry.match(/\[[^\]]+\]/g)[1];
} catch (e) {
areaText.innerText = "-";
}
area.appendChild(areaText);
var logEntry = document.createElement("td");
var logEntryText = document.createElement("span");
try {
logEntryText.innerText = entry.split("]")[2];
} catch (e) {
logEntryText.innerText = "-";
}
logEntry.appendChild(logEntryText);
row.appendChild(timestamp);
row.appendChild(type);
row.appendChild(area);
row.appendChild(logEntry);
//row.classList.add("table-blablubb");
/*
if (logEntryText.innerText.includes('ALSA lib pcm')) {
row.classList.add("table-secondary");
}
*/
if (typeText.innerText.includes("info")) {
row.classList.add("table-info");
}
if (typeText.innerText.includes("debug")) {
row.classList.add("table-secondary");
}
if (typeText.innerText.includes("warning")) {
row.classList.add("table-warning");
}
if (typeText.innerText.includes("error")) {
row.classList.add("table-danger");
}
if (document.getElementById("enable_filter_info").checked) {
row.style.display = "table-row";
display_class("table-info", true);
} else {
row.style.display = "None";
display_class("table-info", false);
}
if (document.getElementById("enable_filter_debug").checked) {
row.style.display = "table-row";
display_class("table-secondary", true);
} else {
row.style.display = "None";
display_class("table-secondary", false);
}
if (document.getElementById("enable_filter_warning").checked) {
row.style.display = "table-row";
display_class("table-warning", true);
} else {
row.style.display = "None";
display_class("table-warning", false);
}
if (document.getElementById("enable_filter_error").checked) {
row.style.display = "table-row";
display_class("table-danger", true);
} else {
row.style.display = "None";
display_class("table-danger", false);
}
tbl.appendChild(row);
// scroll to bottom of page
// https://stackoverflow.com/a/11715670
window.scrollTo(0, document.body.scrollHeight);
});

File diff suppressed because it is too large Load diff

View file

@ -1,231 +0,0 @@
const path = require("path");
const { ipcRenderer } = require("electron");
// https://stackoverflow.com/a/26227660
var appDataFolder =
process.env.APPDATA ||
(process.platform == "darwin"
? process.env.HOME + "/Library/Application Support"
: process.env.HOME + "/.config");
var configFolder = path.join(appDataFolder, "FreeDATA");
var configPath = path.join(configFolder, "config.json");
const config = require(configPath);
var callsignPath = path.join(configFolder, "callsigns.json");
const callsigns = require(callsignPath);
// WINDOW LISTENER
window.addEventListener("DOMContentLoaded", () => {
// startPing button clicked
document
.getElementById("transmit_mesh_ping")
.addEventListener("click", () => {
var dxcallsign = document
.getElementById("dxCallMesh")
.value.toUpperCase();
if (dxcallsign == "" || dxcallsign == null || dxcallsign == undefined)
return;
//pauseButton(document.getElementById("transmit_mesh_ping"), 2000);
ipcRenderer.send("run-tnc-command", {
command: "mesh_ping",
dxcallsign: dxcallsign,
});
});
});
ipcRenderer.on("action-update-mesh-table", (event, arg) => {
var routes = arg.routing_table;
if (typeof routes == "undefined") {
return;
}
var tbl = document.getElementById("mesh-table");
if (tbl !== null) {
tbl.innerHTML = "";
}
for (i = 0; i < routes.length; i++) {
var row = document.createElement("tr");
var datetime = new Date(routes[i]["timestamp"] * 1000).toLocaleString(
navigator.language,
{
hourCycle: "h23",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
},
);
var timestamp = document.createElement("td");
var timestampText = document.createElement("span");
timestampText.innerText = datetime;
timestamp.appendChild(timestampText);
var dxcall = document.createElement("td");
var dxcallText = document.createElement("span");
dxcallText.innerText = routes[i]["dxcall"];
// check for callsign in callsign list, else use checksum
for (let call in callsigns) {
if (callsigns[call] == routes[i]["dxcall"]) {
dxcallText.innerText += " (" + call + ")";
continue;
}
}
dxcall.appendChild(dxcallText);
var router = document.createElement("td");
var routerText = document.createElement("span");
routerText.innerText = routes[i]["router"];
// check for callsign in callsign list, else use checksum
for (let call in callsigns) {
if (callsigns[call] == routes[i]["router"]) {
routerText.innerHTML += `<span class="badge ms-2 bg-secondary">${call}</span>`;
continue;
}
}
router.appendChild(routerText);
var hops = document.createElement("td");
var hopsText = document.createElement("span");
hopsText.innerText = routes[i]["hops"];
hops.appendChild(hopsText);
var score = document.createElement("td");
var scoreText = document.createElement("span");
scoreText.innerText = routes[i]["score"];
score.appendChild(scoreText);
var snr = document.createElement("td");
var snrText = document.createElement("span");
snrText.innerText = routes[i]["snr"];
snr.appendChild(snrText);
row.appendChild(timestamp);
row.appendChild(dxcall);
row.appendChild(router);
row.appendChild(hops);
row.appendChild(score);
row.appendChild(snr);
tbl.appendChild(row);
}
/*-------------------------------------------*/
var routes = arg.mesh_signalling_table;
//console.log(routes);
if (typeof routes == "undefined") {
return;
}
var tbl = document.getElementById("mesh-signalling-table");
if (tbl !== null) {
tbl.innerHTML = "";
}
for (i = 0; i < routes.length; i++) {
var row = document.createElement("tr");
var datetime = new Date(routes[i]["timestamp"] * 1000).toLocaleString(
navigator.language,
{
hourCycle: "h23",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
},
);
var timestamp = document.createElement("td");
var timestampText = document.createElement("span");
timestampText.innerText = datetime;
timestamp.appendChild(timestampText);
var destination = document.createElement("td");
var destinationText = document.createElement("span");
destinationText.innerText = routes[i]["destination"];
// check for callsign in callsign list, else use checksum
for (let call in callsigns) {
if (callsigns[call] == routes[i]["destination"]) {
destinationText.innerHTML += `<span class="badge ms-2 bg-secondary">${call}</span>`;
continue;
}
}
destination.appendChild(destinationText);
var origin = document.createElement("td");
var originText = document.createElement("span");
originText.innerText = routes[i]["origin"];
// check for callsign in callsign list, else use checksum
for (let call in callsigns) {
if (callsigns[call] == routes[i]["origin"]) {
originText.innerHTML += `<span class="badge ms-2 bg-secondary">${call}</span>`;
continue;
}
}
origin.appendChild(originText);
var frametype = document.createElement("td");
var frametypeText = document.createElement("span");
frametypeText.innerText = routes[i]["frametype"];
frametype.appendChild(frametypeText);
var payload = document.createElement("td");
var payloadText = document.createElement("span");
payloadText.innerText = routes[i]["payload"];
payload.appendChild(payloadText);
var attempt = document.createElement("td");
var attemptText = document.createElement("span");
attemptText.innerText = routes[i]["attempt"];
attempt.appendChild(attemptText);
var status = document.createElement("td");
var statusText = document.createElement("span");
//statusText.innerText = routes[i]["status"];
switch (routes[i]["status"]) {
case "acknowledged":
var status_icon = '<i class="bi bi-check-circle-fill"></i>';
var status_color = "bg-success";
break;
case "acknowledging":
var status_icon = '<i class="bi bi-check-circle"></i>';
var status_color = "bg-warning";
break;
case "forwarding":
var status_icon = '<i class="bi bi-arrow-left-right"></i>';
var status_color = "bg-secondary";
break;
case "awaiting_ack":
var status_icon = '<i class="bi bi-clock-history"></i>';
var status_color = "bg-info";
break;
default:
var status_icon = '<i class="bi bi-question-circle-fill"></i>';
var status_color = "bg-primary";
break;
}
statusText.innerHTML = `
<span class="badge ${status_color}">${status_icon}</span>
<span class="badge ${status_color}">${routes[i]["status"]}</span>
`;
status.appendChild(statusText);
row.appendChild(timestamp);
row.appendChild(destination);
row.appendChild(origin);
row.appendChild(frametype);
row.appendChild(payload);
row.appendChild(attempt);
row.appendChild(status);
tbl.appendChild(row);
}
});

View file

@ -1,5 +1,5 @@
var net = require("net");
import { atob_FD, btoa_FD } from "./freedata";
import { atob_FD, btoa_FD, sortByPropertyDesc } from "./freedata";
import { addDataToWaterfall } from "../js/waterfallHandler.js";
import {
@ -18,8 +18,7 @@ setActivePinia(pinia);
import { useStateStore } from "../store/stateStore.js";
const stateStore = useStateStore(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { settingsStore as settings } from "../store/settingsStore.js";
var client = new net.Socket();
var socketchunk = ""; // Current message, per connection.
@ -31,8 +30,10 @@ const split_char = "0;1;";
// global to keep track of Modem connection error emissions
var modemShowConnectStateError = 1;
var setTxAudioLevelOnce = true;
var setRxAudioLevelOnce = true;
// network connection Timeout
setTimeout(connectModem, 2000);
//setTimeout(connectModem, 2000);
function connectModem() {
//exports.connectModem = function(){
@ -171,7 +172,8 @@ client.on("data", function (socketdata) {
stateStore.arq_state = data["arq_state"];
stateStore.mode = data["mode"];
stateStore.bandwidth = data["bandwidth"];
stateStore.tx_audio_level = data["audio_level"];
stateStore.tx_audio_level = data["tx_audio_level"];
stateStore.rx_audio_level = data["rx_audio_level"];
// if audio level is different from config one, send new audio level to modem
//console.log(parseInt(stateStore.tx_audio_level))
//console.log(parseInt(settings.tx_audio_level))
@ -185,6 +187,16 @@ client.on("data", function (socketdata) {
setTxAudioLevel(settings.tx_audio_level);
}
if (
parseInt(stateStore.rx_audio_level) !==
parseInt(settings.rx_audio_level) &&
setRxAudioLevelOnce === true
) {
setRxAudioLevelOnce = false;
console.log(setRxAudioLevelOnce);
setRxAudioLevel(settings.rx_audio_level);
}
stateStore.dbfs_level = data["audio_dbfs"];
stateStore.ptt_state = data["ptt_state"];
stateStore.speed_level = data["speed_level"];
@ -215,7 +227,9 @@ client.on("data", function (socketdata) {
stateStore.dbfs_level = Math.round(stateStore.dbfs_level);
stateStore.arq_total_bytes = data["total_bytes"];
stateStore.heard_stations = data["stations"];
stateStore.heard_stations = data["stations"].sort(
sortByPropertyDesc("timestamp"),
);
stateStore.dxcallsign = data["dxcallsign"];
stateStore.beacon_state = data["beacon_state"];
@ -541,6 +555,11 @@ export function setTxAudioLevel(value) {
'{"type" : "set", "command" : "tx_audio_level", "value" : "' + value + '"}';
writeTncCommand(command);
}
export function setRxAudioLevel(value) {
var command =
'{"type" : "set", "command" : "rx_audio_level", "value" : "' + value + '"}';
writeTncCommand(command);
}
// Send Message
export function sendMessage(obj) {
@ -700,11 +719,6 @@ function sendResponseSharedFile(dxcallsign, sharedFile, sharedFileData) {
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() {
@ -713,21 +727,6 @@ export function getRxBuffer() {
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 =

View file

@ -0,0 +1,53 @@
import { getAudioDevices, getSerialDevices } from "./api";
let audioDevices = await getAudioDevices();
let serialDevices = await getSerialDevices();
//Dummy device data sent if unable to get devices from modem to prevent GUI crash
const skel = JSON.parse(`
[{
"api": "MME",
"id": "0000",
"name": "No devices received from modem",
"native_index": 0
}]`);
export function loadAudioDevices() {
getAudioDevices().then((devices) => {
audioDevices = devices;
});
}
export function loadSerialDevices() {
getSerialDevices().then((devices) => {
serialDevices = devices;
});
}
export function audioInputOptions() {
if (audioDevices === undefined) {
return skel;
}
return audioDevices.in;
}
export function audioOutputOptions() {
if (audioDevices === undefined) {
return skel;
}
return audioDevices.out;
}
export function serialDeviceOptions() {
//Return ignore option if no serialDevices
if (serialDevices === undefined)
return [{ description: "-- ignore --", port: "ignore" }];
if (serialDevices.findIndex((device) => device.port == "ignore") == -1) {
//Add an ignore option for rig and ptt for transceivers that don't require them
serialDevices.push({ description: "-- ignore --", port: "ignore" });
}
return serialDevices;
}

270
gui/src/js/eventHandler.js Normal file
View file

@ -0,0 +1,270 @@
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 { settingsStore as settings } from "../store/settingsStore.js";
export function connectionFailed(endpoint, event) {
stateStore.modem_connection = "disconnected";
}
export function stateDispatcher(data) {
data = JSON.parse(data);
//console.debug(data);
if (data["type"] == "state-change" || data["type"] == "state") {
stateStore.modem_connection = "connected";
stateStore.channel_busy = data["channel_busy"];
stateStore.is_codec2_traffic = data["is_codec2_traffic"];
stateStore.is_modem_running = data["is_modem_running"];
stateStore.dbfs_level = Math.round(data["audio_dbfs"]);
stateStore.dbfs_level_percent = Math.round(
Math.pow(10, data["audio_dbfs"] / 20) * 100,
);
stateStore.s_meter_strength_raw = Math.round(data["s_meter_strength"]);
stateStore.s_meter_strength_percent = Math.round(
Math.pow(10, data["s_meter_strength"] / 20) * 100,
);
stateStore.channel_busy_slot = data["channel_busy_slot"];
stateStore.beacon_state = data["is_beacon_running"];
stateStore.radio_status = data["radio_status"];
stateStore.frequency = data["radio_frequency"];
stateStore.mode = data["radio_mode"];
//Reverse entries so most recent is first
stateStore.activities = Object.entries(data["activities"]).reverse();
build_HSL();
}
}
export function eventDispatcher(data) {
data = JSON.parse(data);
console.debug(data);
if (data["scatter"] !== undefined) {
stateStore.scatter = JSON.parse(data["scatter"]);
return;
}
switch (data["ptt"]) {
case true:
case false:
// get ptt state as a first test
//console.warn("PTT state true")
stateStore.ptt_state = data.ptt;
return;
}
switch (data["modem"]) {
case "started":
displayToast("success", "bi-arrow-left-right", "Modem started", 5000);
return;
case "stopped":
displayToast("success", "bi-arrow-left-right", "Modem stopped", 5000);
return;
case "restarted":
displayToast("secondary", "bi-bootstrap-reboot", "Modem restarted", 5000);
return;
case "failed":
displayToast(
"danger",
"bi-bootstrap-reboot",
"Modem startup failed | bad config?",
5000,
);
return;
}
var message = "";
switch (data["type"]) {
case "hello-client":
message = "Connected to server";
displayToast("success", "bi-ethernet", message, 5000);
stateStore.modem_connection = "connected";
return;
case "arq":
if (data["arq-transfer-outbound"]) {
switch (data["arq-transfer-outbound"].state) {
case "NEW":
message = `Type: ${data.type}, Session ID: ${data["arq-transfer-outbound"].session_id}, DXCall: ${data["arq-transfer-outbound"].dxcall}, Total Bytes: ${data["arq-transfer-outbound"].total_bytes}, State: ${data["arq-transfer-outbound"].state}`;
displayToast("success", "bi-check-circle", message, 5000);
stateStore.dxcallsign = data["arq-transfer-outbound"].dxcall;
stateStore.arq_transmission_percent = 0;
stateStore.arq_total_bytes = 0;
return;
case "OPEN_SENT":
console.info("state OPEN_SENT needs to be implemented");
return;
case "INFO_SENT":
console.info("state INFO_SENT needs to be implemented");
return;
case "BURST_SENT":
message = `Type: ${data.type}, Session ID: ${data["arq-transfer-outbound"].session_id}, DXCall: ${data["arq-transfer-outbound"].dxcall}, Received Bytes: ${data["arq-transfer-outbound"].received_bytes}/${data["arq-transfer-outbound"].total_bytes}, State: ${data["arq-transfer-outbound"].state}`;
displayToast("info", "bi-info-circle", message, 5000);
stateStore.arq_transmission_percent =
(data["arq-transfer-outbound"].received_bytes /
data["arq-transfer-outbound"].total_bytes) *
100;
stateStore.arq_total_bytes =
data["arq-transfer-outbound"].received_bytes;
return;
case "ABORTING":
console.info("state ABORTING needs to be implemented");
return;
case "ABORTED":
message = `Type: ${data.type}, Session ID: ${
data["arq-transfer-outbound"].session_id
}, DXCall: ${data["arq-transfer-outbound"].dxcall}, Total Bytes: ${
data["arq-transfer-outbound"].total_bytes
}, Success: ${
data["arq-transfer-outbound"].success ? "Yes" : "No"
}, State: ${data["arq-transfer-outbound"].state}, Data: ${
data["arq-transfer-outbound"].data ? "Available" : "Not Available"
}`;
displayToast("warning", "bi-exclamation-triangle", message, 5000);
return;
case "FAILED":
message = `Type: ${data.type}, Session ID: ${
data["arq-transfer-outbound"].session_id
}, DXCall: ${data["arq-transfer-outbound"].dxcall}, Total Bytes: ${
data["arq-transfer-outbound"].total_bytes
}, Success: ${
data["arq-transfer-outbound"].success ? "Yes" : "No"
}, State: ${data["arq-transfer-outbound"].state}, Data: ${
data["arq-transfer-outbound"].data ? "Available" : "Not Available"
}`;
displayToast("danger", "bi-x-octagon", message, 5000);
return;
}
}
if (data["arq-transfer-inbound"]) {
switch (data["arq-transfer-inbound"].state) {
case "NEW":
message = `Type: ${data.type}, Session ID: ${data["arq-transfer-inbound"].session_id}, DXCall: ${data["arq-transfer-inbound"].dxcall}, State: ${data["arq-transfer-inbound"].state}`;
displayToast("info", "bi-info-circle", message, 5000);
stateStore.dxcallsign = data["arq-transfer-inbound"].dxcall;
stateStore.arq_transmission_percent = 0;
stateStore.arq_total_bytes = 0;
return;
case "OPEN_ACK_SENT":
message = `Session ID: ${data["arq-transfer-inbound"].session_id}, DXCall: ${data["arq-transfer-inbound"].dxcall}, Total Bytes: ${data["arq-transfer-inbound"].total_bytes}, State: ${data["arq-transfer-inbound"].state}`;
displayToast("info", "bi-arrow-left-right", message, 5000);
stateStore.arq_transmission_percent =
(data["arq-transfer-inbound"].received_bytes /
data["arq-transfer-inbound"].total_bytes) *
100;
stateStore.arq_total_bytes =
data["arq-transfer-inbound"].received_bytes;
return;
case "INFO_ACK_SENT":
message = `Type: ${data.type}, Session ID: ${data["arq-transfer-inbound"].session_id}, DXCall: ${data["arq-transfer-inbound"].dxcall}, Received Bytes: ${data["arq-transfer-inbound"].received_bytes}/${data["arq-transfer-inbound"].total_bytes}, State: ${data["arq-transfer-inbound"].state}`;
displayToast("info", "bi-info-circle", message, 5000);
stateStore.arq_transmission_percent =
(data["arq-transfer-inbound"].received_bytes /
data["arq-transfer-inbound"].total_bytes) *
100;
stateStore.arq_total_bytes =
data["arq-transfer-inbound"].received_bytes;
return;
case "BURST_REPLY_SENT":
message = `Type: ${data.type}, Session ID: ${data["arq-transfer-inbound"].session_id}, DXCall: ${data["arq-transfer-inbound"].dxcall}, Received Bytes: ${data["arq-transfer-inbound"].received_bytes}/${data["arq-transfer-inbound"].total_bytes}, State: ${data["arq-transfer-inbound"].state}`;
displayToast("info", "bi-info-circle", message, 5000);
stateStore.arq_transmission_percent =
(data["arq-transfer-inbound"].received_bytes /
data["arq-transfer-inbound"].total_bytes) *
100;
stateStore.arq_total_bytes =
data["arq-transfer-inbound"].received_bytes;
return;
case "ENDED":
message = `Type: ${data.type}, Session ID: ${data["arq-transfer-inbound"].session_id}, DXCall: ${data["arq-transfer-inbound"].dxcall}, Received Bytes: ${data["arq-transfer-inbound"].received_bytes}/${data["arq-transfer-inbound"].total_bytes}, State: ${data["arq-transfer-inbound"].state}`;
displayToast("info", "bi-info-circle", message, 5000);
// Forward data to chat module
newMessageReceived(
data["arq-transfer-inbound"].data,
data["arq-transfer-inbound"],
);
stateStore.arq_transmission_percent =
(data["arq-transfer-inbound"].received_bytes /
data["arq-transfer-inbound"].total_bytes) *
100;
stateStore.arq_total_bytes =
data["arq-transfer-inbound"].received_bytes;
return;
case "ABORTED":
console.info("state ABORTED needs to be implemented");
return;
case "FAILED":
message = `Type: ${data.type}, Session ID: ${data["arq-transfer-outbound"].session_id}, DXCall: ${data["arq-transfer-outbound"].dxcall}, Received Bytes: ${data["arq-transfer-outbound"].received_bytes}/${data["arq-transfer-outbound"].total_bytes}, State: ${data["arq-transfer-outbound"].state}`;
displayToast("info", "bi-info-circle", message, 5000);
return;
}
}
return;
}
}
function build_HSL() {
//Use data from activities to build HSL list
for (let i = 0; i < stateStore.activities.length; i++) {
if (
stateStore.activities[i][1].direction != "received" ||
stateStore.activities[i][1].origin == undefined
) {
//Ignore stations without origin and not received type
//console.warn("HSL: Ignoring " + stateStore.activities[i][0]);
continue;
}
let found = false;
for (let ii = 0; ii < stateStore.heard_stations.length; ii++) {
if (
stateStore.heard_stations[ii].origin ==
stateStore.activities[i][1].origin
) {
//Station already in HSL, check if newer than one in HSL
found = true;
if (
stateStore.heard_stations[ii].timestamp <
stateStore.activities[i][1].timestamp
) {
//Update existing entry in HSL
stateStore.heard_stations[ii] = stateStore.activities[i][1];
}
}
}
if (found == false) {
//Station not in HSL, let us add it
stateStore.heard_stations.push(stateStore.activities[i][1]);
}
}
stateStore.heard_stations.sort((a, b) => b.timestamp - a.timestamp); // b - a for reverse sort
}

56
gui/src/js/event_sock.js Normal file
View file

@ -0,0 +1,56 @@
import {
eventDispatcher,
stateDispatcher,
connectionFailed,
} from "../js/eventHandler.js";
import { addDataToWaterfall } from "../js/waterfallHandler.js";
// ----------------- init pinia stores -------------
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { settingsStore as settings } from "../store/settingsStore.js";
function connect(endpoint, dispatcher) {
let socket = new WebSocket(
"ws://" + settings.local.host + ":" + settings.local.port + "/" + endpoint,
);
// handle opening
socket.addEventListener("open", function (event) {
console.log("Connected to the WebSocket server: " + endpoint);
});
// handle data
socket.addEventListener("message", function (event) {
dispatcher(event.data);
return;
});
// handle errors
socket.addEventListener("error", function (event) {
console.error("WebSocket error:", event);
connectionFailed(endpoint, event);
});
// handle closing and reconnect
socket.addEventListener("close", function (event) {
console.log("WebSocket connection closed:", event.code);
// Reconnect handler
if (!event.wasClean) {
setTimeout(() => {
console.log("Reconnecting to websocket");
connect(endpoint, dispatcher);
}, 1000);
}
});
}
// Initial connection attempts to endpoints
export function initConnections() {
connect("states", stateDispatcher);
connect("events", eventDispatcher);
connect("fft", addDataToWaterfall);
}

View file

@ -25,3 +25,75 @@ export function atob(data) {
//exports.atob = function (data) {
return window.btoa(Buffer.from(data, "base64").toString("utf8"));
}
//https://medium.com/@asadise/sorting-a-json-array-according-one-property-in-javascript-18b1d22cd9e9
/**
* Sort a json collection by a property ascending
* @param {string} property property to sort on
* @returns sorted json collection
*/
export function sortByProperty(property) {
return function (a, b) {
if (a[property] > b[property]) return 1;
else if (a[property] < b[property]) return -1;
return 0;
};
}
//https://medium.com/@asadise/sorting-a-json-array-according-one-property-in-javascript-18b1d22cd9e9
/**
* Sort a json collection by a property descending
* @param {string} property property to sort on
* @returns sorted json collection
*/
export function sortByPropertyDesc(property) {
return function (a, b) {
if (a[property] < b[property]) return 1;
else if (a[property] > b[property]) return -1;
return 0;
};
}
/**
* Validate a call sign with ssid
* @param {string} callsign callsign to check
* @returns true or false if callsign appears to be valid with an SSID
*/
export function validateCallsignWithSSID(callsign: string) {
var patt = new RegExp("^[A-Z]+[0-9][A-Z]*-(1[0-5]|[0-9])$");
if (
callsign === undefined ||
callsign === "" ||
patt.test(callsign) === false
) {
console.error(
"Call sign given is not in correct format or missing; callsign passed is: " +
callsign,
);
return false;
}
return true;
}
/**
* Validate/check if a call sign has an SSID
* @param {string} callsign callsign to check
* @returns true or false if callsign appears to be valid without an SSID
*/
export function validateCallsignWithoutSSID(callsign: string) {
var patt = new RegExp("^[A-Z]+[0-9][A-Z]+$");
if (
callsign === undefined ||
callsign === "" ||
patt.test(callsign) === false
) {
console.error(
"Call sign given is not in correct format or missing; callsign passed is: " +
callsign,
);
return false;
}
return true;
}

View file

@ -1,13 +1,16 @@
import path from "node:path";
import fs from "fs";
import { setColormap } from "./waterfallHandler";
// pinia store setup
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { settingsStore as settings, onChange } from "../store/settingsStore.js";
import { useStateStore } from "../store/stateStore";
const stateStore = useStateStore(pinia);
// ---------------------------------
console.log(process.env);
@ -47,68 +50,18 @@ if (!fs.existsSync(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" : "",\
"tx_audio_level" : 100,\
"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" \
}';
const configDefaultSettings = `{
"modem_host": "127.0.0.1",
"modem_port": 5000,
"spectrum": "waterfall",
"theme": "default",
"screen_height": 430,
"screen_width": 1050,
"update_channel": "latest",
"wftheme": 2,
"enable_sys_notification": 1
}`;
var parsedConfig = JSON.parse(configDefaultSettings);
if (!fs.existsSync(configPath)) {
fs.writeFileSync(configPath, configDefaultSettings);
@ -123,7 +76,6 @@ export function loadSettings() {
// 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]);
@ -133,9 +85,12 @@ export function loadSettings() {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
}
try {
if (key == "wftheme") {
setColormap();
}
if (key == "mycall") {
settings.mycall = config[key].split("-")[0];
settings.myssid = config[key].split("-")[1];
settings.remote.STATION.mycall = config[key].split("-")[0];
settings.remote.STATION.myssid = config[key].split("-")[1];
} else {
settings[key] = config[key];
}
@ -145,9 +100,46 @@ export function loadSettings() {
}
}
export function saveSettingsToFile() {
console.log("save settings to file...");
let config = settings.getJSON();
console.log(config);
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
//No longer used...
//export function saveSettingsToFile() {
// console.log("save settings to file...");
// let config = settings.getJSON();
// console.log(config);
// fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
//}
export function processModemConfig(data) {
// update our settings from get request
// TODO Can we make this more dynamic? Maybe using a settings object?
// For now its a hardcoded structure until we found a better way
console.log(data);
for (const category in data) {
if (data.hasOwnProperty(category)) {
for (const setting in data[category]) {
if (data[category].hasOwnProperty(setting)) {
// Create a variable name combining the category and setting name
const variableName = setting;
// Assign the value to the variable
if (variableName == "mycall") {
let mycall = data[category][setting];
if (mycall.includes("-")) {
const splittedCallsign = mycall.split("-");
settings.remote.STATION.mycall = splittedCallsign[0]; // The part before the hyphen
settings.remote.STATION.myssid = parseInt(
splittedCallsign[1],
10,
); // The part after the hyphen, converted to a number
} else {
settings.remote.STATION.mycall = mycall; // Use the original mycall if no SSID is present
settings.remote.STATION.myssid = 0; // Default SSID if not provided
}
} else {
settings[variableName] = data[category][setting];
}
console.log(variableName + ": " + settings[variableName]);
}
}
}
}
}

View file

@ -4,26 +4,40 @@ import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { settingsStore as settings } from "../store/settingsStore.js";
var spectrum = new Object();
export function initWaterfall() {
spectrum = new Spectrum("waterfall", {
var spectrums = [];
export function initWaterfall(id) {
spectrum = new Spectrum(id, {
spectrumPercent: 0,
wf_rows: 192, //Assuming 1 row = 1 pixe1, 192 is the height of the spectrum container
wf_rows: 1024, //Assuming 1 row = 1 pixe1, 192 is the height of the spectrum container
wf_size: 1024,
});
console.log(settings.wftheme);
spectrum.setColorMap(settings.wftheme);
spectrum.setColorMap(settings.local.wf_theme);
spectrums.push(spectrum);
return spectrum;
}
export function addDataToWaterfall(data) {
//console.log(spectrum)
try {
spectrum.addData(data);
} catch (e) {
//console.log(e);
}
data = JSON.parse(data);
if (data.constructor !== Array) return;
spectrums.forEach((element) => {
//console.log(element);
element.addData(data);
});
//window.dispatchEvent(new CustomEvent("wf-data-avail", {bubbles:true, detail: data }));
}
/**
* Setwaterfall colormap array by index
* @param {number} index colormap index to use
*/
export function setColormap() {
let index = settings.local.wf_theme;
if (isNaN(index)) index = 0;
console.log("Setting waterfall colormap to " + index);
spectrums.forEach((element) => {
//console.log(element);
element.setColorMap(index);
});
}

View file

@ -1,6 +1,5 @@
import { createApp } from "vue";
import { createPinia } from "pinia";
import { loadSettings } from "./js/settingsHandler";
import "./styles.css";
import { Chart, Filler } from "chart.js";
// Register the Filler plugin globally
@ -30,9 +29,11 @@ const tooltipList = [...tooltipTriggerList].map(
(tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl),
);
loadSettings();
import { getRemote } from "./store/settingsStore";
import { initConnections } from "./js/event_sock.js";
import { getModemState } from "./js/api";
//import './js/settingsHandler.js'
import "./js/daemon";
import "./js/sock.js";
//import './js/settingsHandler.js'
getRemote().then(() => {
initConnections();
getModemState();
});

View file

@ -1,55 +0,0 @@
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

@ -42,9 +42,9 @@ export const useChatStore = defineStore("chatStore", () => {
});
var inputText = ref("");
var inputFile = ref();
var inputFileName = ref();
var inputFileType = ref();
var inputFileSize = ref();
var inputFileName = ref("-");
var inputFileType = ref("-");
var inputFileSize = ref("-");
var callsign_list = ref();
var sorted_chat_list = ref();

View file

@ -1,231 +1,126 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { reactive, ref, watch } from "vue";
export const useSettingsStore = defineStore("settingsStore", () => {
// audio
var tx_audio = ref();
var rx_audio = ref();
var tx_audio_level = ref();
import { getConfig, setConfig } from "../js/api";
// 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);
var nconf = require("nconf");
nconf.file({ file: "config/config.json" });
// 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,
tx_audio_level: tx_audio_level.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,
tx_audio_level,
};
// +++
//GUI DEFAULT SETTINGS........
//Set GUI defaults here, they will be used if not found in config/config.json
//They should be an exact mirror (variable wise) of settingsStore.local
//Nothing else should be needed aslong as components are using v-bind
// +++
nconf.defaults({
local: {
host: "127.0.0.1",
port: "5000",
spectrum: "waterfall",
wf_theme: 2,
update_channel: "alpha",
enable_sys_notification: false,
grid_layout: "[]",
grid_preset: "[]",
grid_enabled: true,
},
});
nconf.required(["local:host", "local:port"]);
export const settingsStore = reactive({
local: {
host: "127.0.0.1",
port: "5000",
spectrum: "waterfall",
wf_theme: 2,
update_channel: "alpha",
enable_sys_notification: false,
grid_layout: "[]",
grid_preset: "[]",
grid_enabled: true,
},
remote: {
AUDIO: {
enable_auto_tune: false,
input_device: "",
output_device: "",
rx_audio_level: 0,
tx_audio_level: 0,
},
MESH: {
enable_protocol: false,
},
MODEM: {
enable_fft: false,
enable_fsk: false,
enable_low_bandwidth_mode: false,
respond_to_cq: false,
rx_buffer_size: 0,
tuning_range_fmax: 0,
tuning_range_fmin: 0,
tx_delay: 0,
beacon_interval: 0,
enable_hamc: false,
enable_morse_identifier: false,
},
RADIO: {
control: "disabled",
model_id: 0,
serial_port: "",
serial_speed: "",
data_bits: 0,
stop_bits: 0,
serial_handshake: "",
ptt_port: "",
ptt_type: "",
serial_dcd: "",
serial_dtr: "",
},
RIGCTLD: {
ip: "127.0.0.1",
port: 0,
path: "",
command: "",
arguments: "",
},
STATION: {
enable_explorer: false,
enable_stats: false,
mycall: "",
myssid: 0,
mygrid: "",
ssid_list: [],
},
TCI: {
tci_ip: "127.0.0.1",
tci_port: 0,
},
},
});
//Save settings for GUI to config file
settingsStore.local = nconf.get("local");
saveLocalSettingsToConfig();
export function onChange() {
setConfig(settingsStore.remote).then((conf) => {
settingsStore.remote = conf;
});
}
export function getRemote() {
return getConfig().then((conf) => {
settingsStore.remote = conf;
});
}
watch(settingsStore.local, (oldValue, newValue) => {
//This function watches for changes, and triggers a save of local settings
saveLocalSettingsToConfig();
});
export function saveLocalSettingsToConfig() {
nconf.set("local", settingsStore.local);
nconf.save();
//console.log("Settings saved!");
}

View file

@ -5,42 +5,46 @@ 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 frequency = ref(0);
var new_frequency = ref(14093000);
var mode = ref("-");
var rf_level = ref("10");
var bandwidth = ref("-");
var dbfs_level_percent = ref(0);
var dbfs_level = ref(0);
var radio_status = ref(false);
var ptt_state = ref("False");
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 channel_busy = ref(false);
var channel_busy_slot = ref([false, false, false, false, false]);
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 is_modem_running = ref();
var arq_total_bytes = ref(0);
var arq_transmission_percent = ref(0);
var heard_stations = ref("");
var activities = ref([]);
var heard_stations = ref([]);
var dxcallsign = ref("");
var arq_session_state = ref("");
var arq_state = ref("");
var beacon_state = ref("False");
var beacon_state = ref(false);
var audio_recording = ref("");
var audio_recording = ref(false);
var hamlib_status = ref("");
var tx_audio_level = ref("");
var rx_audio_level = ref("");
var alc = ref("");
var is_codec2_traffic = ref("");
@ -61,78 +65,12 @@ export const useStateStore = defineStore("stateStore", () => {
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();
}
}
@ -150,7 +88,6 @@ export const useStateStore = defineStore("stateStore", () => {
fft,
channel_busy,
channel_busy_slot,
getChannelBusySlotState,
scatter,
ptt_state,
s_meter_strength_percent,
@ -159,6 +96,7 @@ export const useStateStore = defineStore("stateStore", () => {
audio_recording,
hamlib_status,
tx_audio_level,
rx_audio_level,
alc,
updateTncState,
arq_transmission_percent,
@ -168,10 +106,12 @@ export const useStateStore = defineStore("stateStore", () => {
arq_seconds_until_finish,
arq_seconds_until_timeout,
arq_seconds_until_timeout_percent,
modem_running_state,
modem_connection,
is_modem_running,
arq_session_state,
is_codec2_traffic,
rf_level,
activities,
heard_stations,
beacon_state,
rigctld_started,
@ -179,5 +119,6 @@ export const useStateStore = defineStore("stateStore", () => {
python_version,
modem_version,
rx_buffer_length,
radio_status,
};
});

8
gui/tests/api.test.js Normal file
View file

@ -0,0 +1,8 @@
import { expect, test } from "vitest";
import { buildURL } from "../src/js/api";
test("builds URLs correctly"),
() => {
const params = { host: "127.0.0.1", port: 123 };
expect(buildURL(params, "/config")).toBe("http://127.0.0.1/config");
};

View file

@ -15,6 +15,14 @@ export default defineConfig(({ command }) => {
const sourcemap = isServe || !!process.env.VSCODE_DEBUG;
return {
optimizeDeps: {
esbuildOptions: {
target: "esnext",
},
},
build: {
target: "esnext",
},
plugins: [
vue(),
electron([

5
modem/api_validations.py Normal file
View file

@ -0,0 +1,5 @@
import re
def validate_freedata_callsign(callsign):
regexp = "^[a-zA-Z]+\d+\w+-\d{1,2}$"
return re.compile(regexp).match(callsign) is not None

121
modem/arq_session.py Normal file
View file

@ -0,0 +1,121 @@
import queue, threading
import codec2
import data_frame_factory
import structlog
from event_manager import EventManager
from modem_frametypes import FRAME_TYPE
import time
class ARQSession():
SPEED_LEVEL_DICT = {
0: {
'mode': codec2.FREEDV_MODE.datac4,
'min_snr': -10,
'duration_per_frame': 5.17,
},
1: {
'mode': codec2.FREEDV_MODE.datac3,
'min_snr': 0,
'duration_per_frame': 3.19,
},
2: {
'mode': codec2.FREEDV_MODE.datac1,
'min_snr': 3,
'duration_per_frame': 4.18,
},
}
def __init__(self, config: dict, modem, dxcall: str):
self.logger = structlog.get_logger(type(self).__name__)
self.config = config
self.event_manager: EventManager = modem.event_manager
self.snr = []
self.dxcall = dxcall
self.dx_snr = []
self.modem = modem
self.speed_level = 0
self.frames_per_burst = 1
self.frame_factory = data_frame_factory.DataFrameFactory(self.config)
self.event_frame_received = threading.Event()
self.id = None
self.session_started = time.time()
self.session_ended = 0
self.session_max_age = 500
def log(self, message, isWarning = False):
msg = f"[{type(self).__name__}][id={self.id}][state={self.state}]: {message}"
logger = self.logger.warn if isWarning else self.logger.info
logger(msg)
def get_mode_by_speed_level(self, speed_level):
return self.SPEED_LEVEL_DICT[speed_level]["mode"]
def transmit_frame(self, frame: bytearray, mode='auto'):
self.log("Transmitting frame")
if mode in ['auto']:
mode = self.get_mode_by_speed_level(self.speed_level)
self.modem.transmit(mode, 1, 1, frame)
def set_state(self, state):
if self.state == state:
self.log(f"{type(self).__name__} state {self.state.name} unchanged.")
else:
self.log(f"{type(self).__name__} state change from {self.state.name} to {state.name}")
self.state = state
def get_data_payload_size(self):
return self.frame_factory.get_available_data_payload_for_mode(
FRAME_TYPE.ARQ_BURST_FRAME,
self.SPEED_LEVEL_DICT[self.speed_level]["mode"]
)
def set_details(self, snr, frequency_offset):
self.snr.append(snr)
self.frequency_offset = frequency_offset
def on_frame_received(self, frame):
self.event_frame_received.set()
self.log(f"Received {frame['frame_type']}")
frame_type = frame['frame_type_int']
if self.state in self.STATE_TRANSITION:
if frame_type in self.STATE_TRANSITION[self.state]:
action_name = self.STATE_TRANSITION[self.state][frame_type]
getattr(self, action_name)(frame)
return
self.log(f"Ignoring unknow transition from state {self.state.name} with frame {frame['frame_type']}")
def is_session_outdated(self):
session_alivetime = time.time() - self.session_max_age
if self.session_ended < session_alivetime and self.state.name in ['FAILED', 'ENDED', 'ABORTED']:
return True
return False
def calculate_session_duration(self):
return self.session_ended - self.session_started
def calculate_session_statistics(self):
duration = self.calculate_session_duration()
total_bytes = self.total_length
# self.total_length
duration_in_minutes = duration / 60 # Convert duration from seconds to minutes
# Calculate bytes per minute
if duration_in_minutes > 0:
bytes_per_minute = int(total_bytes / duration_in_minutes)
else:
bytes_per_minute = 0
return {
'total_bytes': total_bytes,
'duration': duration,
'bytes_per_minute': bytes_per_minute
}

233
modem/arq_session_irs.py Normal file
View file

@ -0,0 +1,233 @@
import threading
import arq_session
import helpers
from modem_frametypes import FRAME_TYPE
from codec2 import FREEDV_MODE
from enum import Enum
import time
class IRS_State(Enum):
NEW = 0
OPEN_ACK_SENT = 1
INFO_ACK_SENT = 2
BURST_REPLY_SENT = 3
ENDED = 4
FAILED = 5
ABORTED = 6
class ARQSessionIRS(arq_session.ARQSession):
TIMEOUT_CONNECT = 55 #14.2
TIMEOUT_DATA = 60
STATE_TRANSITION = {
IRS_State.NEW: {
FRAME_TYPE.ARQ_SESSION_OPEN.value : 'send_open_ack',
FRAME_TYPE.ARQ_STOP.value: 'send_stop_ack'
},
IRS_State.OPEN_ACK_SENT: {
FRAME_TYPE.ARQ_SESSION_OPEN.value: 'send_open_ack',
FRAME_TYPE.ARQ_SESSION_INFO.value: 'send_info_ack',
FRAME_TYPE.ARQ_STOP.value: 'send_stop_ack'
},
IRS_State.INFO_ACK_SENT: {
FRAME_TYPE.ARQ_SESSION_INFO.value: 'send_info_ack',
FRAME_TYPE.ARQ_BURST_FRAME.value: 'receive_data',
FRAME_TYPE.ARQ_STOP.value: 'send_stop_ack'
},
IRS_State.BURST_REPLY_SENT: {
FRAME_TYPE.ARQ_BURST_FRAME.value: 'receive_data',
FRAME_TYPE.ARQ_STOP.value: 'send_stop_ack'
},
IRS_State.ENDED: {
FRAME_TYPE.ARQ_BURST_FRAME.value: 'receive_data',
FRAME_TYPE.ARQ_STOP.value: 'send_stop_ack'
},
IRS_State.FAILED: {
FRAME_TYPE.ARQ_BURST_FRAME.value: 'receive_data',
FRAME_TYPE.ARQ_STOP.value: 'send_stop_ack'
},
IRS_State.ABORTED: {
FRAME_TYPE.ARQ_STOP.value: 'send_stop_ack',
FRAME_TYPE.ARQ_SESSION_OPEN.value: 'send_open_ack',
FRAME_TYPE.ARQ_SESSION_INFO.value: 'send_info_ack',
FRAME_TYPE.ARQ_BURST_FRAME.value: 'receive_data',
},
}
def __init__(self, config: dict, modem, dxcall: str, session_id: int):
super().__init__(config, modem, dxcall)
self.id = session_id
self.dxcall = dxcall
self.version = 1
self.state = IRS_State.NEW
self.state_enum = IRS_State # needed for access State enum from outside
self.total_length = 0
self.total_crc = ''
self.received_data = None
self.received_bytes = 0
self.received_crc = None
self.transmitted_acks = 0
self.abort = False
def set_decode_mode(self):
self.modem.demodulator.set_decode_mode(self.get_mode_by_speed_level(self.speed_level))
def all_data_received(self):
return self.total_length == self.received_bytes
def final_crc_matches(self) -> bool:
match = self.total_crc == helpers.get_crc_32(bytes(self.received_data)).hex()
return match
def transmit_and_wait(self, frame, timeout, mode):
self.event_frame_received.clear()
self.transmit_frame(frame, mode)
self.log(f"Waiting {timeout} seconds...")
if not self.event_frame_received.wait(timeout):
self.log("Timeout waiting for ISS. Session failed.")
self.session_ended = time.time()
self.set_state(IRS_State.FAILED)
self.event_manager.send_arq_session_finished(False, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics())
def launch_transmit_and_wait(self, frame, timeout, mode):
thread_wait = threading.Thread(target = self.transmit_and_wait,
args = [frame, timeout, mode], daemon=True)
thread_wait.start()
def send_open_ack(self, open_frame):
self.event_manager.send_arq_session_new(
False, self.id, self.dxcall, 0, self.state.name)
ack_frame = self.frame_factory.build_arq_session_open_ack(
self.id,
self.dxcall,
self.version,
self.snr[0], flag_abort=self.abort)
self.launch_transmit_and_wait(ack_frame, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling)
if not self.abort:
self.set_state(IRS_State.OPEN_ACK_SENT)
def send_info_ack(self, info_frame):
# Get session info from ISS
self.received_data = bytearray(info_frame['total_length'])
self.total_length = info_frame['total_length']
self.total_crc = info_frame['total_crc']
self.dx_snr.append(info_frame['snr'])
self.log(f"New transfer of {self.total_length} bytes")
self.event_manager.send_arq_session_new(False, self.id, self.dxcall, self.total_length, self.state.name)
self.calibrate_speed_settings()
self.set_decode_mode()
info_ack = self.frame_factory.build_arq_session_info_ack(
self.id, self.total_crc, self.snr[0],
self.speed_level, self.frames_per_burst, flag_abort=self.abort)
self.launch_transmit_and_wait(info_ack, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling)
if not self.abort:
self.set_state(IRS_State.INFO_ACK_SENT)
def process_incoming_data(self, frame):
if frame['offset'] != self.received_bytes:
self.log(f"Discarding data offset {frame['offset']}")
return False
remaining_data_length = self.total_length - self.received_bytes
# Is this the last data part?
if remaining_data_length <= len(frame['data']):
# we only want the remaining length, not the entire frame data
data_part = frame['data'][:remaining_data_length]
else:
# we want the entire frame data
data_part = frame['data']
self.received_data[frame['offset']:] = data_part
self.received_bytes += len(data_part)
self.log(f"Received {self.received_bytes}/{self.total_length} bytes")
self.event_manager.send_arq_session_progress(
False, self.id, self.dxcall, self.received_bytes, self.total_length, self.state.name)
return True
def receive_data(self, burst_frame):
self.process_incoming_data(burst_frame)
self.calibrate_speed_settings()
if not self.all_data_received():
ack = self.frame_factory.build_arq_burst_ack(
self.id, self.received_bytes,
self.speed_level, self.frames_per_burst, self.snr[0], flag_abort=self.abort)
self.set_decode_mode()
# increase ack counter
# self.transmitted_acks += 1
self.set_state(IRS_State.BURST_REPLY_SENT)
self.launch_transmit_and_wait(ack, self.TIMEOUT_DATA, mode=FREEDV_MODE.signalling)
return
if self.final_crc_matches():
self.log("All data received successfully!")
ack = self.frame_factory.build_arq_burst_ack(self.id,
self.received_bytes,
self.speed_level,
self.frames_per_burst,
self.snr[0],
flag_final=True,
flag_checksum=True)
self.transmit_frame(ack, mode=FREEDV_MODE.signalling)
self.log("ACK sent")
self.session_ended = time.time()
self.set_state(IRS_State.ENDED)
self.event_manager.send_arq_session_finished(
False, self.id, self.dxcall, True, self.state.name, data=self.received_data, statistics=self.calculate_session_statistics())
else:
ack = self.frame_factory.build_arq_burst_ack(self.id,
self.received_bytes,
self.speed_level,
self.frames_per_burst,
self.snr[0],
flag_final=True,
flag_checksum=False)
self.transmit_frame(ack, mode=FREEDV_MODE.signalling)
self.log("CRC fail at the end of transmission!")
self.session_ended = time.time()
self.set_state(IRS_State.FAILED)
self.event_manager.send_arq_session_finished(
False, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics())
def calibrate_speed_settings(self):
self.speed_level = 0 # for now stay at lowest speed level
return
# if we have two ACKS, then consider increasing speed level
if self.transmitted_acks >= 2:
self.transmitted_acks = 0
new_speed_level = min(self.speed_level + 1, len(self.SPEED_LEVEL_DICT) - 1)
# check first if the next mode supports the actual snr
if self.snr[0] >= self.SPEED_LEVEL_DICT[new_speed_level]["min_snr"]:
self.speed_level = new_speed_level
def abort_transmission(self):
self.log(f"Aborting transmission... setting abort flag")
self.abort = True
def send_stop_ack(self, stop_frame):
stop_ack = self.frame_factory.build_arq_stop_ack(self.id)
self.launch_transmit_and_wait(stop_ack, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling)
self.set_state(IRS_State.ABORTED)
self.event_manager.send_arq_session_finished(
False, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics())

205
modem/arq_session_iss.py Normal file
View file

@ -0,0 +1,205 @@
import threading
import data_frame_factory
import queue
import random
from codec2 import FREEDV_MODE
from modem_frametypes import FRAME_TYPE
import arq_session
import helpers
from enum import Enum
import time
class ISS_State(Enum):
NEW = 0
OPEN_SENT = 1
INFO_SENT = 2
BURST_SENT = 3
ENDED = 4
FAILED = 5
ABORTING = 6 # state while running abort sequence and waiting for stop ack
ABORTED = 7 # stop ack received
class ARQSessionISS(arq_session.ARQSession):
RETRIES_CONNECT = 10
# DJ2LS: 3 seconds seems to be too small for radios with a too slow PTT toggle time
# DJ2LS: 3.5 seconds is working well WITHOUT a channel busy detection delay
TIMEOUT_CHANNEL_BUSY = 2
TIMEOUT_CONNECT_ACK = 3.5 + TIMEOUT_CHANNEL_BUSY
TIMEOUT_TRANSFER = 3.5 + TIMEOUT_CHANNEL_BUSY
TIMEOUT_STOP_ACK = 3.5 + TIMEOUT_CHANNEL_BUSY
STATE_TRANSITION = {
ISS_State.OPEN_SENT: {
FRAME_TYPE.ARQ_SESSION_OPEN_ACK.value: 'send_info',
},
ISS_State.INFO_SENT: {
FRAME_TYPE.ARQ_SESSION_OPEN_ACK.value: 'send_info',
FRAME_TYPE.ARQ_SESSION_INFO_ACK.value: 'send_data',
},
ISS_State.BURST_SENT: {
FRAME_TYPE.ARQ_SESSION_INFO_ACK.value: 'send_data',
FRAME_TYPE.ARQ_BURST_ACK.value: 'send_data',
},
ISS_State.FAILED:{
FRAME_TYPE.ARQ_STOP_ACK.value: 'transmission_aborted'
},
ISS_State.ABORTING: {
FRAME_TYPE.ARQ_STOP_ACK.value: 'transmission_aborted',
},
ISS_State.ABORTED: {
FRAME_TYPE.ARQ_STOP_ACK.value: 'transmission_aborted',
}
}
def __init__(self, config: dict, modem, dxcall: str, data: bytearray, state_manager):
super().__init__(config, modem, dxcall)
self.state_manager = state_manager
self.data = data
self.total_length = len(data)
self.data_crc = ''
self.confirmed_bytes = 0
self.state = ISS_State.NEW
self.state_enum = ISS_State # needed for access State enum from outside
self.id = self.generate_id()
self.frame_factory = data_frame_factory.DataFrameFactory(self.config)
def generate_id(self):
while True:
random_int = random.randint(1,255)
if random_int not in self.state_manager.arq_iss_sessions:
return random_int
if len(self.state_manager.arq_iss_sessions) >= 255:
return False
def transmit_wait_and_retry(self, frame_or_burst, timeout, retries, mode):
while retries > 0:
self.event_frame_received = threading.Event()
if isinstance(frame_or_burst, list): burst = frame_or_burst
else: burst = [frame_or_burst]
for f in burst:
self.transmit_frame(f, mode)
self.event_frame_received.clear()
self.log(f"Waiting {timeout} seconds...")
if self.event_frame_received.wait(timeout):
return
self.log("Timeout!")
retries = retries - 1
self.set_state(ISS_State.FAILED)
self.transmission_failed()
def launch_twr(self, frame_or_burst, timeout, retries, mode):
twr = threading.Thread(target = self.transmit_wait_and_retry, args=[frame_or_burst, timeout, retries, mode], daemon=True)
twr.start()
def start(self):
self.event_manager.send_arq_session_new(
True, self.id, self.dxcall, self.total_length, self.state.name)
session_open_frame = self.frame_factory.build_arq_session_open(self.dxcall, self.id)
self.launch_twr(session_open_frame, self.TIMEOUT_CONNECT_ACK, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)
self.set_state(ISS_State.OPEN_SENT)
def set_speed_and_frames_per_burst(self, frame):
self.speed_level = frame['speed_level']
self.log(f"Speed level set to {self.speed_level}")
self.frames_per_burst = frame['frames_per_burst']
self.log(f"Frames per burst set to {self.frames_per_burst}")
def send_info(self, irs_frame):
# check if we received an abort flag
if irs_frame["flag"]["ABORT"]:
self.transmission_aborted(irs_frame)
return
info_frame = self.frame_factory.build_arq_session_info(self.id, self.total_length,
helpers.get_crc_32(self.data),
self.snr[0])
self.launch_twr(info_frame, self.TIMEOUT_CONNECT_ACK, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)
self.set_state(ISS_State.INFO_SENT)
def send_data(self, irs_frame):
self.set_speed_and_frames_per_burst(irs_frame)
if 'offset' in irs_frame:
self.confirmed_bytes = irs_frame['offset']
self.log(f"IRS confirmed {self.confirmed_bytes}/{self.total_length} bytes")
self.event_manager.send_arq_session_progress(
True, self.id, self.dxcall, self.confirmed_bytes, self.total_length, self.state.name)
# check if we received an abort flag
if irs_frame["flag"]["ABORT"]:
self.transmission_aborted(irs_frame)
return
if irs_frame["flag"]["FINAL"]:
if self.confirmed_bytes == self.total_length and irs_frame["flag"]["CHECKSUM"]:
self.transmission_ended(irs_frame)
return
else:
self.transmission_failed()
return
payload_size = self.get_data_payload_size()
burst = []
for f in range(0, self.frames_per_burst):
offset = self.confirmed_bytes
payload = self.data[offset : offset + payload_size]
data_frame = self.frame_factory.build_arq_burst_frame(
self.SPEED_LEVEL_DICT[self.speed_level]["mode"],
self.id, self.confirmed_bytes, payload)
burst.append(data_frame)
self.launch_twr(burst, self.TIMEOUT_TRANSFER, self.RETRIES_CONNECT, mode='auto')
self.set_state(ISS_State.BURST_SENT)
def transmission_ended(self, irs_frame):
# final function for sucessfully ended transmissions
self.session_ended = time.time()
self.set_state(ISS_State.ENDED)
self.log(f"All data transfered! flag_final={irs_frame['flag']['FINAL']}, flag_checksum={irs_frame['flag']['CHECKSUM']}")
self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,True, self.state.name, statistics=self.calculate_session_statistics())
self.state_manager.remove_arq_iss_session(self.id)
def transmission_failed(self, irs_frame=None):
# final function for failed transmissions
self.session_ended = time.time()
self.set_state(ISS_State.FAILED)
self.log(f"Transmission failed!")
self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,False, self.state.name, statistics=self.calculate_session_statistics())
def abort_transmission(self, irs_frame=None):
# function for starting the abort sequence
self.log(f"aborting transmission...")
self.set_state(ISS_State.ABORTING)
self.event_manager.send_arq_session_finished(
True, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics())
# break actual retries
self.event_frame_received.set()
# start with abort sequence
self.send_stop()
def send_stop(self):
stop_frame = self.frame_factory.build_arq_stop(self.id)
self.launch_twr(stop_frame, self.TIMEOUT_STOP_ACK, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)
def transmission_aborted(self, irs_frame):
self.log("session aborted")
self.session_ended = time.time()
self.set_state(ISS_State.ABORTED)
# break actual retries
self.event_frame_received.set()
self.event_manager.send_arq_session_finished(
True, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics())
self.state_manager.remove_arq_iss_session(self.id)

View file

@ -3,10 +3,12 @@ Gather information about audio devices.
"""
import atexit
import multiprocessing
import crcengine
import sounddevice as sd
import structlog
import numpy as np
import queue
import threading
atexit.register(sd._terminate)
@ -28,8 +30,8 @@ def get_audio_devices():
# we need to reset and initialize sounddevice before running the multiprocessing part.
# If we are not doing this at this early point, not all devices will be displayed
sd._terminate()
sd._initialize()
#sd._terminate()
#sd._initialize()
# log.debug("[AUD] get_audio_devices")
with multiprocessing.Manager() as manager:
@ -49,15 +51,10 @@ def get_audio_devices():
def device_crc(device) -> str:
crc_hwid = crc_algorithm(bytes(f"{device}", encoding="utf-8"))
crc_hwid = crc_algorithm(bytes(f"{device['name']}.{device['hostapi']}", encoding="utf-8"))
crc_hwid = crc_hwid.to_bytes(2, byteorder="big")
crc_hwid = crc_hwid.hex()
hostapi_name = sd.query_hostapis(device['hostapi'])['name']
return f"{device['name']} [{hostapi_name}] [{crc_hwid}]"
return crc_hwid
def fetch_audio_devices(input_devices, output_devices):
"""
@ -91,13 +88,243 @@ def fetch_audio_devices(input_devices, output_devices):
max_output_channels = 0
if max_input_channels > 0:
new_input_device = {"id": index, "name": device_crc(device)}
hostapi_name = sd.query_hostapis(device['hostapi'])['name']
new_input_device = {"id": device_crc(device),
"name": device['name'],
"api": hostapi_name,
"native_index":index}
# check if device not in device list
if new_input_device not in input_devices:
input_devices.append(new_input_device)
if max_output_channels > 0:
new_output_device = {"id": index, "name": device_crc(device)}
hostapi_name = sd.query_hostapis(device['hostapi'])['name']
new_output_device = {"id": device_crc(device),
"name": device['name'],
"api": hostapi_name,
"native_index":index}
# check if device not in device list
if new_output_device not in output_devices:
output_devices.append(new_output_device)
# FreeData uses the crc as id inside the configuration
# SD lib uses a numerical id which is essentially an
# index of the device within the list
# returns (id, name)
def get_device_index_from_crc(crc, isInput: bool):
try:
in_devices = []
out_devices = []
fetch_audio_devices(in_devices, out_devices)
if isInput:
detected_devices = in_devices
else:
detected_devices = out_devices
for i, dev in enumerate(detected_devices):
if dev['id'] == crc:
return (dev['native_index'], dev['name'])
except Exception as e:
log.warning(f"Audio device {crc} not detected ", devices=detected_devices, isInput=isInput)
return [None, None]
def test_audio_devices(input_id: str, output_id: str) -> list:
test_result = [False, False]
try:
result = get_device_index_from_crc(input_id, True)
if result is None:
# in_dev_index, in_dev_name = None, None
raise ValueError(f"[Audio-Test] Invalid input device index {input_id}.")
else:
in_dev_index, in_dev_name = result
sd.check_input_settings(
device=in_dev_index,
channels=1,
dtype="int16",
samplerate=48000,
)
test_result[0] = True
except (sd.PortAudioError, ValueError) as e:
log.warning(f"[Audio-Test] Input device error ({input_id}):", e=e)
test_result[0] = False
try:
result = get_device_index_from_crc(output_id, False)
if result is None:
# out_dev_index, out_dev_name = None, None
raise ValueError(f"[Audio-Test] Invalid output device index {output_id}.")
else:
out_dev_index, out_dev_name = result
sd.check_output_settings(
device=out_dev_index,
channels=1,
dtype="int16",
samplerate=48000,
)
test_result[1] = True
except (sd.PortAudioError, ValueError) as e:
log.warning(f"[Audio-Test] Output device error ({output_id}):", e=e)
test_result[1] = False
sd._terminate()
sd._initialize()
return test_result
def set_audio_volume(datalist: np.ndarray, dB: float) -> np.ndarray:
"""
Scale values for the provided audio samples by dB.
:param datalist: Audio samples to scale
:type datalist: np.ndarray
:param dB: Decibels to scale samples, constrained to the range [-50, 50]
:type dB: float
:return: Scaled audio samples
:rtype: np.ndarray
"""
try:
dB = float(dB)
except ValueError as e:
print(f"[MDM] Changing audio volume failed with error: {e}")
dB = 0.0 # 0 dB means no change
# Clip dB value to the range [-50, 50]
dB = np.clip(dB, -30, 20)
# Ensure datalist is an np.ndarray
if not isinstance(datalist, np.ndarray):
print("[MDM] Invalid data type for datalist. Expected np.ndarray.")
return datalist
# Convert dB to linear scale
scale_factor = 10 ** (dB / 20)
# Scale samples
scaled_data = datalist * scale_factor
# Clip values to int16 range and convert data type
return np.clip(scaled_data, -32768, 32767).astype(np.int16)
RMS_COUNTER = 0
CHANNEL_BUSY_DELAY = 0
def calculate_fft(data, fft_queue, states) -> None:
"""
Calculate an average signal strength of the channel to assess
whether the channel is "busy."
"""
# Initialize dbfs counter
# rms_counter = 0
# https://gist.github.com/ZWMiller/53232427efc5088007cab6feee7c6e4c
# Fast Fourier Transform, 10*log10(abs) is to scale it to dB
# and make sure it's not imaginary
global RMS_COUNTER, CHANNEL_BUSY_DELAY
try:
fftarray = np.fft.rfft(data)
# Set value 0 to 1 to avoid division by zero
fftarray[fftarray == 0] = 1
dfft = 10.0 * np.log10(abs(fftarray))
# get average of dfft
avg = np.mean(dfft)
# Detect signals which are higher than the
# average + 10 (+10 smoothes the output).
# Data higher than the average must be a signal.
# Therefore we are setting it to 100 so it will be highlighted
# Have to do this when we are not transmitting so our
# own sending data will not affect this too much
if not states.isTransmitting():
dfft[dfft > avg + 15] = 100
# Calculate audio dbfs
# https://stackoverflow.com/a/9763652
# calculate dbfs every 50 cycles for reducing CPU load
RMS_COUNTER += 1
if RMS_COUNTER > 5:
d = np.frombuffer(data, np.int16).astype(np.float32)
# calculate RMS and then dBFS
# https://dsp.stackexchange.com/questions/8785/how-to-compute-dbfs
# try except for avoiding runtime errors by division/0
try:
rms = int(np.sqrt(np.max(d ** 2)))
if rms == 0:
raise ZeroDivisionError
audio_dbfs = 20 * np.log10(rms / 32768)
states.set("audio_dbfs", audio_dbfs)
except Exception as e:
states.set("audio_dbfs", -100)
RMS_COUNTER = 0
# Convert data to int to decrease size
dfft = dfft.astype(int)
# Create list of dfft
dfftlist = dfft.tolist()
# Reduce area where the busy detection is enabled
# We want to have this in correlation with mode bandwidth
# TODO This is not correctly and needs to be checked for correct maths
# dfftlist[0:1] = 10,15Hz
# Bandwidth[Hz] / 10,15
# narrowband = 563Hz = 56
# wideband = 1700Hz = 167
# 1500Hz = 148
# 2700Hz = 266
# 3200Hz = 315
# slot
slot = 0
slot1 = [0, 65]
slot2 = [65,120]
slot3 = [120, 176]
slot4 = [176, 231]
slot5 = [231, len(dfftlist)]
slotbusy = [False,False,False,False,False]
# Set to true if we should increment delay count; else false to decrement
addDelay=False
for range in [slot1, slot2, slot3, slot4, slot5]:
range_start = range[0]
range_end = range[1]
# define the area, we are detecting busy state
slotdfft = dfft[range_start:range_end]
# Check for signals higher than average by checking for "100"
# If we have a signal, increment our channel_busy delay counter
# so we have a smoother state toggle
if np.sum(slotdfft[slotdfft > avg + 15]) >= 200 and not states.isTransmitting():
addDelay=True
slotbusy[slot]=True
#states.channel_busy_slot[slot] = True
# increment slot
slot += 1
states.set_channel_slot_busy(slotbusy)
if addDelay:
# Limit delay counter to a maximum of 200. The higher this value,
# the longer we will wait until releasing state
states.set_channel_busy_condition_traffic(True)
CHANNEL_BUSY_DELAY = min(CHANNEL_BUSY_DELAY + 10, 200)
else:
# Decrement channel busy counter if no signal has been detected.
CHANNEL_BUSY_DELAY = max(CHANNEL_BUSY_DELAY - 1, 0)
# When our channel busy counter reaches 0, toggle state to False
if CHANNEL_BUSY_DELAY == 0:
states.set_channel_busy_condition_traffic(False)
# erase queue if greater than 3
if fft_queue.qsize() >= 1:
fft_queue = queue.Queue()
fft_queue.put(dfftlist[:315]) # 315 --> bandwidth 3200
except Exception as err:
print(f"[MDM] calculate_fft: Exception: {err}")

41
modem/beacon.py Normal file
View file

@ -0,0 +1,41 @@
import command_beacon
import sched
import time
import threading
class Beacon:
def __init__(self, config, states, event_manager, logger, modem):
self.config = config
self.states = states
self.event_manager = event_manager
self.log = logger
self.modem = modem
self.scheduler = sched.scheduler(time.time, time.sleep)
self.beacon_interval = self.config['MODEM']['beacon_interval']
self.beacon_enabled = False
self.event = threading.Event()
def start(self):
self.beacon_enabled = True
self.schedule_beacon()
def stop(self):
self.beacon_enabled = False
def schedule_beacon(self):
if self.beacon_enabled:
self.scheduler.enter(self.beacon_interval, 1, self.run_beacon)
threading.Thread(target=self.scheduler.run, daemon=True).start()
def run_beacon(self):
if self.beacon_enabled:
# Your beacon logic here
cmd = command_beacon.BeaconCommand(self.config, self.states, self.event_manager)
cmd.run(self.event_manager, self.modem)
self.schedule_beacon() # Reschedule the next beacon
def refresh(self):
# Interrupt and reschedule the beacon
self.scheduler = sched.scheduler(time.time, time.sleep)
self.schedule_beacon()

View file

@ -1,118 +0,0 @@
import structlog
import threading
import helpers
import time
import modem
import base64
from global_instances import ARQ, AudioParam, Beacon, Channel, Daemon, HamlibParam, ModemParam, Station, Statistics, TCIParam, Modem
import sock
import ujson as json
class broadcastHandler:
"""Terminal Node Controller for FreeDATA"""
log = structlog.get_logger("BROADCAST")
def __init__(self) -> None:
self.fec_wakeup_callsign = bytes()
self.longest_duration = 6
self.wakeup_received = False
self.broadcast_timeout_reached = False
self.broadcast_payload_bursts = 1
self.broadcast_watchdog = threading.Thread(
target=self.watchdog, name="watchdog thread", daemon=True
)
self.broadcast_watchdog.start()
def received_fec_wakeup(self, data_in: bytes):
self.fec_wakeup_callsign = helpers.bytes_to_callsign(bytes(data_in[1:7]))
self.wakeup_mode = int.from_bytes(bytes(data_in[7:8]), "big")
bursts = int.from_bytes(bytes(data_in[8:9]), "big")
self.wakeup_received = True
modem.RECEIVE_DATAC4 = True
self.send_data_to_socket_queue(
freedata="modem-message",
fec="wakeup",
mode=self.wakeup_mode,
bursts=bursts,
dxcallsign=str(self.fec_wakeup_callsign, "UTF-8")
)
self.log.info(
"[Modem] FRAME WAKEUP RCVD ["
+ str(self.fec_wakeup_callsign, "UTF-8")
+ "] ", mode=self.wakeup_mode, bursts=bursts,
)
def received_fec(self, data_in: bytes):
print(self.fec_wakeup_callsign)
self.send_data_to_socket_queue(
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("[Modem] FEC DATA RCVD")
def send_data_to_socket_queue(self, **jsondata):
"""
Send information to the UI via JSON and the sock.SOCKET_QUEUE.
Args:
Dictionary containing the data to be sent, in the format:
key=value, for each item. E.g.:
self.send_data_to_socket_queue(
freedata="modem-message",
arq="received",
status="success",
uuid=self.transmission_uuid,
timestamp=timestamp,
mycallsign=str(self.mycallsign, "UTF-8"),
dxcallsign=str(Station.dxcallsign, "UTF-8"),
dxgrid=str(Station.dxgrid, "UTF-8"),
data=base64_data,
)
"""
# add mycallsign and dxcallsign to network message if they not exist
# and make sure we are not overwrite them if they exist
try:
if "mycallsign" not in jsondata:
jsondata["mycallsign"] = str(Station.mycallsign, "UTF-8")
if "dxcallsign" not in jsondata:
jsondata["dxcallsign"] = str(Station.dxcallsign, "UTF-8")
except Exception as 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("[Modem] send_data_to_socket_queue:", jsondata=json_data_out)
# finally push data to our network queue
sock.SOCKET_QUEUE.put(json_data_out)
def watchdog(self):
while 1:
if self.wakeup_received:
timeout = time.time() + (self.longest_duration * self.broadcast_payload_bursts) + 2
while time.time() < timeout:
threading.Event().wait(0.01)
self.broadcast_timeout_reached = True
self.log.info(
"[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
modem.RECEIVE_DATAC4 = False
self.fec_wakeup_callsign = bytes()
self.wakeup_received = False
else:
threading.Event().wait(0.01)

View file

@ -24,16 +24,12 @@ class FREEDV_MODE(Enum):
"""
Enumeration for codec2 modes and names
"""
sig0 = 19
sig1 = 19
signalling = 19
datac0 = 14
datac1 = 10
datac3 = 12
datac4 = 18
datac13 = 19
fsk_ldpc = 9
fsk_ldpc_0 = 200
fsk_ldpc_1 = 201
class FREEDV_MODE_USED_SLOTS(Enum):
@ -105,13 +101,14 @@ for file in files:
#log.info("[C2 ] Libcodec2 loaded", path=file)
break
except OSError as err:
log.warning("[C2 ] Error: Libcodec2 found but not loaded", path=file, e=err)
pass
#log.info("[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 ] Error: Libcodec2 not loaded - Exiting")
sys.exit(1)
log.info("[C2 ] Libcodec2 loaded...")
# ctypes function init
# api.freedv_set_tuning_range.restype = ctypes.c_int
@ -425,3 +422,46 @@ class resampler:
self.filter_mem8 = in8_mem[: self.MEM8]
return out48
def open_instance(mode: int) -> ctypes.c_void_p:
"""
Return a codec2 instance of the type `mode`
:param mode: Type of codec2 instance to return
:type mode: Union[int, str]
:return: C-function of the requested codec2 instance
:rtype: ctypes.c_void_p
"""
# if mode in [FREEDV_MODE.fsk_ldpc_0.value]:
# return ctypes.cast(
# api.freedv_open_advanced(
# FREEDV_MODE.fsk_ldpc.value,
# ctypes.byref(api.FREEDV_MODE_FSK_LDPC_0_ADV),
# ),
# ctypes.c_void_p,
# )
#
# if mode in [FREEDV_MODE.fsk_ldpc_1.value]:
# return ctypes.cast(
# api.freedv_open_advanced(
# FREEDV_MODE.fsk_ldpc.value,
# ctypes.byref(api.FREEDV_MODE_FSK_LDPC_1_ADV),
# ),
# ctypes.c_void_p,
# )
#
return ctypes.cast(api.freedv_open(mode), ctypes.c_void_p)
def get_bytes_per_frame(mode: int) -> int:
"""
Provide bytes per frame information for accessing from data handler
:param mode: Codec2 mode to query
:type mode: int or str
:return: Bytes per frame of the supplied codec2 data mode
:rtype: int
"""
freedv = open_instance(mode)
# TODO add close session
# get number of bytes per frame for mode
return int(api.freedv_get_bits_per_modem_frame(freedv) / 8)

56
modem/command.py Normal file
View file

@ -0,0 +1,56 @@
from data_frame_factory import DataFrameFactory
import queue
from codec2 import FREEDV_MODE
import structlog
from state_manager import StateManager
class TxCommand():
def __init__(self, config: dict, state_manager: StateManager, event_manager, apiParams:dict = {}):
self.config = config
self.logger = structlog.get_logger("Command")
self.state_manager = state_manager
self.event_manager = event_manager
self.set_params_from_api(apiParams)
self.frame_factory = DataFrameFactory(config)
def set_params_from_api(self, apiParams):
pass
def get_name(self):
return type(self).__name__
def emit_event(self, event_queue):
pass
def log_message(self):
return f"Running {self.get_name()}"
def build_frame(self):
pass
def get_tx_mode(self):
return FREEDV_MODE.signalling
def make_modem_queue_item(self, mode, repeat, repeat_delay, frame):
return {
'mode': mode,
'repeat': repeat,
'repeat_delay': repeat_delay,
'frame': frame,
}
def transmit(self, modem):
frame = self.build_frame()
modem.transmit(self.get_tx_mode(), 1, 0, frame)
def run(self, event_queue: queue.Queue, modem):
self.emit_event(event_queue)
self.logger.info(self.log_message())
self.transmit(modem)
def test(self, event_queue: queue.Queue):
self.emit_event(event_queue)
self.logger.info(self.log_message())
frame = self.build_frame()
return frame

27
modem/command_arq_raw.py Normal file
View file

@ -0,0 +1,27 @@
import queue
from command import TxCommand
import api_validations
import base64
from queue import Queue
from arq_session_iss import ARQSessionISS
class ARQRawCommand(TxCommand):
def set_params_from_api(self, apiParams):
self.dxcall = apiParams['dxcall']
if not api_validations.validate_freedata_callsign(self.dxcall):
self.dxcall = f"{self.dxcall}-0"
self.data = base64.b64decode(apiParams['data'])
def run(self, event_queue: Queue, modem):
self.emit_event(event_queue)
self.logger.info(self.log_message())
iss = ARQSessionISS(self.config, modem, self.dxcall, self.data, self.state_manager)
if iss.id:
self.state_manager.register_arq_iss_session(iss)
iss.start()
return iss
return False

13
modem/command_beacon.py Normal file
View file

@ -0,0 +1,13 @@
from command import TxCommand
class BeaconCommand(TxCommand):
def build_frame(self):
return self.frame_factory.build_beacon()
#def transmit(self, modem):
# super().transmit(modem)
# if self.config['MODEM']['enable_morse_identifier']:
# mycall = f"{self.config['STATION']['mycall']}-{self.config['STATION']['myssid']}"
# modem.transmit_morse("morse", 1, 0, mycall)

6
modem/command_cq.py Normal file
View file

@ -0,0 +1,6 @@
from command import TxCommand
class CQCommand(TxCommand):
def build_frame(self):
return self.frame_factory.build_cq()

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