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

5
.gitignore vendored
View file

@ -29,4 +29,7 @@ coverage.xml
#Ignore gui build items #Ignore gui build items
/gui/dist /gui/dist
/gui/release /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": [ "files": [
"dist", "dist",
"dist-electron", "dist-electron",
"../modem/dist/modem/", "../modem/server.dist/",
], ],
"extraResources": [ "extraResources": [
{ {
"from": "../modem/dist/modem/", "from": "../modem/server.dist/",
"to": "modem", "to": "modem",
"filter": [ "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 process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL
? join(process.env.DIST_ELECTRON, "../public") ? join(process.env.DIST_ELECTRON, "../public")
: process.env.DIST; : process.env.DIST;
process.env.FDUpdateAvail = "0";
// Disable GPU Acceleration for Windows 7 // Disable GPU Acceleration for Windows 7
if (release().startsWith("6.1")) app.disableHardwareAcceleration(); if (release().startsWith("6.1")) app.disableHardwareAcceleration();
@ -55,8 +56,7 @@ async function createWindow() {
autoHideMenuBar: true, autoHideMenuBar: true,
webPreferences: { webPreferences: {
preload, preload,
backgroundThrottle: false, backgroundThrottling: false,
// Warning: Enable nodeIntegration and disable contextIsolation is not secure in production // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
// Consider using contextBridge.exposeInMainWorld // Consider using contextBridge.exposeInMainWorld
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
@ -100,20 +100,20 @@ async function createWindow() {
app.whenReady().then(() => { app.whenReady().then(() => {
createWindow(); createWindow();
console.log(platform()) console.log(platform());
//Generate daemon binary path //Generate daemon binary path
var daemonPath = ""; var daemonPath = "";
switch (platform().toLowerCase()) { switch (platform().toLowerCase()) {
case "darwin": case "darwin":
daemonPath = join(process.resourcesPath, "modem", "freedata-daemon"); daemonPath = join(process.resourcesPath, "modem", "freedata-server");
case "linux": case "linux":
daemonPath = join(process.resourcesPath, "modem", "freedata-daemon"); daemonPath = join(process.resourcesPath, "modem", "freedata-server");
break; break;
case "win32": case "win32":
daemonPath = join(process.resourcesPath, "modem", "freedata-daemon.exe"); daemonPath = join(process.resourcesPath, "modem", "freedata-server.exe");
break; break;
case "win64": case "win64":
daemonPath = join(process.resourcesPath, "modem", "freedata-daemon.exe"); daemonPath = join(process.resourcesPath, "modem", "freedata-server.exe");
break; break;
default: default:
console.log("Unhandled OS Platform: ", platform()); console.log("Unhandled OS Platform: ", platform());
@ -122,24 +122,22 @@ console.log(platform())
//Start daemon binary if it exists //Start daemon binary if it exists
if (existsSync(daemonPath)) { if (existsSync(daemonPath)) {
console.log("Starting freedata-daemon binary"); console.log("Starting freedata-server binary");
console.log("daemonPath:", daemonPath); console.log("daemonPath:", daemonPath);
console.log("CWD:", join(daemonPath, "..")); console.log("CWD:", join(daemonPath, ".."));
/* /*
var daemonProcess = spawn("freedata-daemon", [], { var daemonProcess = spawn("freedata-server", [], {
cwd: join(process.env.DIST, "modem"), cwd: join(process.env.DIST, "modem"),
shell: true shell: true
}); });
*/ */
/* /*
daemonProcess = spawn(daemonPath, [], { daemonProcess = spawn(daemonPath, [], {
shell: true shell: true
}); });
console.log(daemonProcess) console.log(daemonProcess)
*/ */
daemonProcess = spawn(daemonPath, [], { daemonProcess = spawn(daemonPath, [], {});
});
// return process messages // return process messages
daemonProcess.on("error", (err) => { daemonProcess.on("error", (err) => {
@ -154,7 +152,7 @@ daemonProcess = spawn(daemonPath, [], {
}); });
daemonProcess.stderr.on("data", (data) => { daemonProcess.stderr.on("data", (data) => {
// daemonProcessLog.info(`${data}`); // daemonProcessLog.info(`${data}`);
console.log(data) console.log(data);
}); });
daemonProcess.on("close", (code) => { daemonProcess.on("close", (code) => {
// daemonProcessLog.warn(`daemonProcess exited with code ${code}`); // daemonProcessLog.warn(`daemonProcess exited with code ${code}`);
@ -170,7 +168,7 @@ daemonProcess = spawn(daemonPath, [], {
app.on("window-all-closed", () => { app.on("window-all-closed", () => {
win = null; win = null;
if (process.platform !== "darwin") app.quit(close_sub_processes()); if (process.platform !== "darwin") app.quit();
}); });
app.on("second-instance", () => { app.on("second-instance", () => {
@ -215,6 +213,7 @@ ipcMain.on("request-restart-and-install-update", (event, data) => {
// LISTENER FOR UPDATER EVENTS // LISTENER FOR UPDATER EVENTS
autoUpdater.on("update-available", (info) => { autoUpdater.on("update-available", (info) => {
process.env.FDUpdateAvail = "1";
console.log("update available"); console.log("update available");
let arg = { let arg = {
@ -234,6 +233,7 @@ autoUpdater.on("update-not-available", (info) => {
}); });
autoUpdater.on("update-downloaded", (info) => { autoUpdater.on("update-downloaded", (info) => {
process.env.FDUpdateAvail = "1";
console.log("update downloaded"); console.log("update downloaded");
let arg = { let arg = {
status: "update-downloaded", status: "update-downloaded",
@ -288,19 +288,19 @@ function close_sub_processes() {
console.log("closing modem and daemon"); console.log("closing modem and daemon");
try { try {
if (platform() == "win32" || platform() == "win64") { if (platform() == "win32") {
spawn("Taskkill", ["/IM", "freedata-modem.exe", "/F"]); spawn("Taskkill", ["/IM", "freedata-modem.exe", "/F"]);
spawn("Taskkill", ["/IM", "freedata-daemon.exe", "/F"]); spawn("Taskkill", ["/IM", "freedata-server.exe", "/F"]);
} }
if (platform() == "linux") { if (platform() == "linux") {
spawn("pkill", ["-9", "freedata-modem"]); spawn("pkill", ["-9", "freedata-modem"]);
spawn("pkill", ["-9", "freedata-daemon"]); spawn("pkill", ["-9", "freedata-server"]);
} }
if (platform() == "darwin") { if (platform() == "darwin") {
spawn("pkill", ["-9", "freedata-modem"]); spawn("pkill", ["-9", "freedata-modem"]);
spawn("pkill", ["-9", "freedata-daemon"]); spawn("pkill", ["-9", "freedata-server"]);
} }
} catch (e) { } catch (e) {
console.log(e); console.log(e);

View file

@ -113,17 +113,6 @@ window.onmessage = (ev) => {
setTimeout(removeLoading, 4999); 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 // IPC ACTION FOR AUTO UPDATER
ipcRenderer.on("action-updater", (event, arg) => { ipcRenderer.on("action-updater", (event, arg) => {
if (arg.status == "download-progress") { if (arg.status == "download-progress") {

View file

@ -7,6 +7,7 @@
"scripts": { "scripts": {
"start": "vite", "start": "vite",
"dev": "vite", "dev": "vite",
"test": "vitest --run",
"check": "vue-tsc --noEmit", "check": "vue-tsc --noEmit",
"build": "vue-tsc --noEmit && vite build && electron-builder -p never", "build": "vue-tsc --noEmit && vite build && electron-builder -p never",
"release": "vue-tsc --noEmit && vite build && electron-builder -p onTag", "release": "vue-tsc --noEmit && vite build && electron-builder -p onTag",
@ -31,55 +32,59 @@
}, },
"homepage": "https://freedata.app", "homepage": "https://freedata.app",
"dependencies": { "dependencies": {
"@electron/asar": "^3.2.7", "@electron/asar": "3.2.7",
"@electron/notarize": "^2.1.0", "@electron/notarize": "2.2.0",
"@electron/universal": "^2.0.0", "@electron/universal": "2.0.0",
"@popperjs/core": "^2.11.8", "@popperjs/core": "2.11.8",
"@vueuse/electron": "^10.4.1", "@vueuse/electron": "10.7.1",
"blob-util": "^2.0.2", "blob-util": "2.0.2",
"bootstrap": "^5.3.1", "bootstrap": "5.3.2",
"bootstrap-icons": "^1.10.5", "bootstrap-icons": "1.11.2",
"bootswatch": "^5.3.1", "bootswatch": "5.3.2",
"browser-image-compression": "^2.0.2", "browser-image-compression": "2.0.2",
"chart.js": "^4.3.3", "chart.js": "4.4.1",
"chartjs-plugin-annotation": "^3.0.1", "chartjs-plugin-annotation": "3.0.1",
"electron-log": "^5.0.0", "electron-log": "5.0.3",
"electron-updater": "^6.1.6", "electron-updater": "6.1.7",
"emoji-picker-element": "^1.18.3", "emoji-picker-element": "1.20.1",
"emoji-picker-element-data": "^1.4.0", "emoji-picker-element-data": "1.6.0",
"file-saver": "^2.0.5", "file-saver": "2.0.5",
"mime": "^3.0.0", "gridstack": "10.0.1",
"pinia": "^2.1.6", "mime": "4.0.1",
"pouchdb": "^8.0.1", "nconf": "^0.12.1",
"pouchdb-browser": "^8.0.1", "pinia": "2.1.7",
"pouchdb-find": "^8.0.1", "pouchdb": "8.0.1",
"pouchdb-upsert": "^2.2.0", "pouchdb-browser": "8.0.1",
"qth-locator": "^2.1.0", "pouchdb-find": "8.0.1",
"sass": "^1.66.1", "pouchdb-upsert": "2.2.0",
"socket.io": "^4.7.2", "qth-locator": "2.1.0",
"uuid": "^9.0.0", "sass": "1.66.1",
"vue": "^3.3.4", "socket.io": "4.7.2",
"vue-chartjs": "^5.2.0", "uuid": "9.0.1",
"vuemoji-picker": "^0.2.0" "vue": "3.3.12",
"vue-chartjs": "5.3.0",
"vuemoji-picker": "0.2.0"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.7.4", "@types/nconf": "^0.10.6",
"@vitejs/plugin-vue": "^4.4.0", "@typescript-eslint/eslint-plugin": "6.17.0",
"electron": "^27.0.0", "@vitejs/plugin-vue": "4.5.2",
"electron-builder": "^24.6.3", "electron": "28.1.3",
"eslint": "^8.50.0", "electron-builder": "24.9.1",
"eslint-config-prettier": "^9.0.0", "eslint": "8.56.0",
"eslint-config-standard-with-typescript": "^40.0.0", "eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "^2.28.1", "eslint-config-standard-with-typescript": "43.0.0",
"eslint-plugin-n": "^16.1.0", "eslint-plugin-import": "2.29.1",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-n": "16.1.0",
"eslint-plugin-promise": "^6.1.1", "eslint-plugin-prettier": "5.0.1",
"eslint-plugin-vue": "^9.17.0", "eslint-plugin-promise": "6.1.1",
"typescript": "^5.2.2", "eslint-plugin-vue": "9.20.1",
"vite": "^5.0.2", "typescript": "5.3.3",
"vite-plugin-electron": "^0.15.4", "vite": "5.0.10",
"vite-plugin-electron-renderer": "^0.14.5", "vite-plugin-electron": "0.28.0",
"vue": "^3.3.4", "vite-plugin-electron-renderer": "0.14.5",
"vue-tsc": "^1.4.2" "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 () { Spectrum.prototype.resize = function () {
var width = this.canvas.clientWidth; var width = this.parent.clientWidth;
var height = this.canvas.clientHeight; var height =this.parent.clientHeight;
// little helper for setting height of clientHeight is not working as expected // little helper for setting height of clientHeight is not working as expected
if (height == 0){ if (height == 0){
var height = 250 var height = 250
} }
if (width == 0){
width=500;
}
if (this.canvas.width != width || this.canvas.height != height) { if (this.canvas.width != width || this.canvas.height != height) {
this.canvas.width = width; this.canvas.width = width;
@ -445,8 +447,8 @@ Spectrum.prototype.onKeypress = function (e) {
export function Spectrum(id, options) { export function Spectrum(id, options) {
console.log("waterfall init....") // console.log("waterfall init....")
console.log(document.getElementById(id)) //console.log(document.getElementById(id))
// Handle options // Handle options
this.centerHz = options && options.centerHz ? options.centerHz : 1500; 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 // Create main canvas and adjust dimensions to match actual
this.canvas = document.getElementById(id); this.canvas = document.getElementById(id);
this.parent = this.canvas.parentElement;
this.canvas.height = this.canvas.clientHeight; this.canvas.height = this.canvas.clientHeight;
this.canvas.width = this.canvas.clientWidth; this.canvas.width = this.canvas.clientWidth;

View file

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

View file

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

View file

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

View file

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

View file

@ -3,12 +3,27 @@ import { setActivePinia } from "pinia";
import pinia from "../store/index"; import pinia from "../store/index";
setActivePinia(pinia); setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js"; import { useStateStore } from "../store/stateStore.js";
const settings = useSettingsStore(pinia); 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> </script>
<template> <template>
<div class="card mb-0"> <div class="card m-2">
<div class="card-header p-1 d-flex"> <div class="card-header p-1 d-flex">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
@ -55,7 +70,7 @@ const settings = useSettingsStore(pinia);
type="button" type="button"
disabled disabled
> >
{{ settings.update_channel }} Update channel:&nbsp; {{ settingsStore.local.update_channel }}
</button> </button>
<button <button
class="btn btn-secondary btn-sm ms-1" 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_modals from "./main_modals.vue";
import main_top_navbar from "./main_top_navbar.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_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 settings_view from "./settings.vue";
import main_active_rig_control from "./main_active_rig_control.vue"; import main_active_rig_control from "./main_active_rig_control.vue";
import main_footer_navbar from "./main_footer_navbar.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 chat from "./chat.vue";
import infoScreen from "./infoScreen.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() { function stopAllTransmissions() {
console.log("stopping transmissions"); console.log("stopping transmissions");
@ -55,16 +54,19 @@ function stopAllTransmissions() {
role="tablist" role="tablist"
style="margin-top: 100px" style="margin-top: 100px"
> >
<main_modem_healthcheck />
<a <a
class="list-group-item list-group-item-dark list-group-item-action border-0 rounded-3 mb-2 active" 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" data-bs-toggle="list"
href="#list-modem" href="#list-grid"
role="tab" role="tab"
aria-controls="list-modem" aria-controls="list-grid"
title="Home" title="Grid"
><i class="bi bi-house-door-fill h3"></i ><i class="bi bi-grid h3"></i
></a> ></a>
<a <a
class="list-group-item list-group-item-dark list-group-item-action border-0 rounded-3 mb-2" class="list-group-item list-group-item-dark list-group-item-action border-0 rounded-3 mb-2"
id="list-chat-list" id="list-chat-list"
@ -106,7 +108,6 @@ function stopAllTransmissions() {
aria-controls="list-logger" aria-controls="list-logger"
><i class="bi bi-activity h3"></i ><i class="bi bi-activity h3"></i
></a> ></a>
<a <a
class="list-group-item list-group-item-dark list-group-item-action border-0 rounded-3 mb-2" class="list-group-item list-group-item-dark list-group-item-action border-0 rounded-3 mb-2"
id="list-settings-list" id="list-settings-list"
@ -117,33 +118,24 @@ function stopAllTransmissions() {
title="Settings" title="Settings"
><i class="bi bi-gear-wide-connected h3"></i ><i class="bi bi-gear-wide-connected h3"></i
></a> ></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>
</div> </div>
<div class="col-sm min-vh-100 m-0 p-0"> <div class="col-sm min-vh-100 m-0 p-0">
<!-- content --> <!-- content -->
<!-- TODO: Remove the top navbar entirely if not needed
<main_top_navbar />
-->
<div class="tab-content" id="nav-tabContent-settings"> <div class="tab-content" id="nav-tabContent-settings">
<div <div
class="tab-pane fade show active" class="tab-pane fade"
id="list-modem" id="list-home"
role="tabpanel" role="tabpanel"
aria-labelledby="list-modem-list" aria-labelledby="list-home-list"
> >
<!-- TOP NAVBAR --> <!-- TOP NAVBAR -->
<main_top_navbar />
<div <div
id="blurdiv" id="blurdiv"
style=" style="
@ -156,51 +148,25 @@ function stopAllTransmissions() {
<!-------------------------------- MAIN AREA ----------------> <!-------------------------------- 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="container">
<div class="row collapse multi-collapse" id="collapseThirdRow"> <div class="row">
<main_active_rig_control />
<div class="col-5"> <div class="col-5">
<main_active_audio_level /> <main_active_rig_control />
</div> </div>
<div class="col"> <div class="col-4">
<main_active_broadcasts /> <main_active_broadcasts />
</div> </div>
<div class="col-3">
<main_active_audio_level />
</div>
</div> </div>
<div <div class="row">
class="row collapse multi-collapse mt-3" <div class="col-7">
id="collapseFourthRow" <main_active_heard_stations />
> </div>
<div class="col-5"> <div class="col-5">
<main_active_stats /> <main_active_stats />
</div> </div>
<div class="col">
<main_active_heard_stations />
</div>
</div> </div>
</div> </div>
</div> </div>
@ -349,6 +315,15 @@ function stopAllTransmissions() {
> >
<infoScreen /> <infoScreen />
</div> </div>
<div
class="tab-pane fade show active"
id="list-grid"
role="tabpanel"
aria-labelledby="list-grid-list"
>
<Dynamic_components />
</div>
<div <div
class="tab-pane fade" class="tab-pane fade"
id="list-chat" id="list-chat"

View file

@ -5,12 +5,6 @@ setActivePinia(pinia);
import { useStateStore } from "../store/stateStore.js"; import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia); const state = useStateStore(pinia);
import { record_audio } from "../js/sock.js";
function startStopRecordAudio() {
record_audio();
}
</script> </script>
<template> <template>
<div class="card mb-1"> <div class="card mb-1">
@ -21,7 +15,7 @@ function startStopRecordAudio() {
<i class="bi bi-volume-up" style="font-size: 1.2rem"></i> <i class="bi bi-volume-up" style="font-size: 1.2rem"></i>
</div> </div>
<div class="col-3"> <div class="col-3">
<strong class="fs-5">Audio</strong> <strong>Audio</strong>
</div> </div>
<div class="col-7"> <div class="col-7">
<button <button
@ -33,18 +27,6 @@ function startStopRecordAudio() {
> >
Tune Tune
</button> </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>
<div class="col-1 text-end"> <div class="col-1 text-end">
<button <button

View file

@ -1,37 +1,26 @@
<script setup lang="ts"> <script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler";
import { setActivePinia } from "pinia"; import { setActivePinia } from "pinia";
import pinia from "../store/index"; import pinia from "../store/index";
setActivePinia(pinia); setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js"; import { settingsStore as settings} from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { useStateStore } from "../store/stateStore.js"; import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia); const state = useStateStore(pinia);
import { sendCQ, sendPing, startBeacon, stopBeacon } from "../js/sock.js"; import { sendModemCQ, sendModemPing, setModemBeacon } from "../js/api.js";
function transmitCQ() {
sendCQ();
}
function transmitPing() { function transmitPing() {
sendPing((<HTMLInputElement>document.getElementById("dxCall")).value); sendModemPing((<HTMLInputElement>document.getElementById("dxCall")).value);
} }
function startStopBeacon() { function startStopBeacon() {
switch (state.beacon_state) { if (state.beacon_state === true) {
case "False": setModemBeacon(false);
startBeacon(settings.beacon_interval); }
else {
break; setModemBeacon(true);
case "True":
stopBeacon();
break;
default:
} }
} }
</script> </script>
@ -94,7 +83,7 @@ function startStopBeacon() {
id="sendCQ" id="sendCQ"
type="button" type="button"
title="Send a CQ to the world" title="Send a CQ to the world"
@click="transmitCQ()" @click="sendModemCQ()"
> >
Call CQ Call CQ
</button> </button>
@ -105,8 +94,8 @@ function startStopBeacon() {
class="btn btn-sm ms-1" class="btn btn-sm ms-1"
@click="startStopBeacon()" @click="startStopBeacon()"
v-bind:class="{ v-bind:class="{
'btn-success': state.beacon_state === 'True', 'btn-success': state.beacon_state === true,
'btn-outline-secondary': state.beacon_state === 'False', '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." 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"; import pinia from "../store/index";
setActivePinia(pinia); setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js"; import { settingsStore as settings } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { useStateStore } from "../store/stateStore.js"; import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia); const state = useStateStore(pinia);
@ -30,9 +29,9 @@ function getDateTime(timestampRaw) {
function getMaidenheadDistance(dxGrid) { function getMaidenheadDistance(dxGrid) {
try { try {
return parseInt(distance(settings.mygrid, dxGrid)); return parseInt(distance(settings.remote.STATION.mygrid, dxGrid));
} catch (e) { } catch (e) {
// console.warn(e);
} }
} }
</script> </script>
@ -82,7 +81,7 @@ function getMaidenheadDistance(dxGrid) {
</thead> </thead>
<tbody id="heardstations"> <tbody id="heardstations">
<!--https://vuejs.org/guide/essentials/list.html--> <!--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> <td>
<span class="badge bg-secondary">{{ <span class="badge bg-secondary">{{
getDateTime(item.timestamp) getDateTime(item.timestamp)
@ -94,18 +93,18 @@ function getMaidenheadDistance(dxGrid) {
> >
</td> </td>
<td> <td>
<span class="badge bg-secondary">{{ item.dxcallsign }}</span> <span class="badge bg-secondary">{{ item.origin }}</span>
</td> </td>
<td> <td>
<span class="badge bg-secondary">{{ item.dxgrid }}</span> <span class="badge bg-secondary">{{ item.gridsquare }}</span>
</td> </td>
<td> <td>
<span class="badge bg-secondary" <span class="badge bg-secondary"
>{{ getMaidenheadDistance(item.dxgrid) }} km</span >{{ getMaidenheadDistance(item.gridsquare) }} km</span
> >
</td> </td>
<td> <td>
<span class="badge bg-secondary">{{ item.datatype }}</span> <span class="badge bg-secondary">{{ item.activity_type }}</span>
</td> </td>
<td> <td>
<span class="badge bg-secondary">{{ item.snr }}</span> <span class="badge bg-secondary">{{ item.snr }}</span>

View file

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

View file

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

View file

@ -5,6 +5,8 @@ setActivePinia(pinia);
import { useAudioStore } from "../store/audioStore.js"; import { useAudioStore } from "../store/audioStore.js";
const audio = useAudioStore(pinia); const audio = useAudioStore(pinia);
import { setConfig } from "../js/api";
</script> </script>
<template> <template>
@ -21,7 +23,6 @@ const audio = useAudioStore(pinia);
<div class="col-1 text-end"> <div class="col-1 text-end">
<button <button
type="button" type="button"
id="openHelpModalAudio"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#audioHelpModal" data-bs-target="#audioHelpModal"
class="btn m-0 p-0 border-0" class="btn m-0 p-0 border-0"
@ -40,9 +41,9 @@ const audio = useAudioStore(pinia);
</span> </span>
<select <select
class="form-select form-select-sm" class="form-select form-select-sm"
id="audio_input_selectbox"
aria-label=".form-select-sm" aria-label=".form-select-sm"
v-html="audio.getInputDevices()" v-html="audio.getInputDevices()"
@change="setConfig"
></select> ></select>
</div> </div>
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
@ -51,9 +52,9 @@ const audio = useAudioStore(pinia);
</span> </span>
<select <select
class="form-select form-select-sm" class="form-select form-select-sm"
id="audio_output_selectbox"
aria-label=".form-select-sm" aria-label=".form-select-sm"
v-html="audio.getOutputDevices()" v-html="audio.getOutputDevices()"
@change="setConfig"
></select> ></select>
</div> </div>
</div> </div>

View file

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

View file

@ -4,29 +4,20 @@
import { setActivePinia } from "pinia"; import { setActivePinia } from "pinia";
import pinia from "../store/index"; import pinia from "../store/index";
setActivePinia(pinia); setActivePinia(pinia);
import { saveSettingsToFile } from "../js/settingsHandler";
import { useChatStore } from "../store/chatStore.js"; import { useChatStore } from "../store/chatStore.js";
const chat = useChatStore(pinia); const chat = useChatStore(pinia);
import { useSettingsStore } from "../store/settingsStore.js"; import { settingsStore as settings, onChange } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { sendModemTestFrame } from "../js/api";
import { import {
deleteChatByCallsign, deleteChatByCallsign,
getNewMessagesByDXCallsign, getNewMessagesByDXCallsign,
} from "../js/chatHandler"; } from "../js/chatHandler";
import { sendTestFrame, setTxAudioLevel } from "../js/sock.js"; import main_startup_check from "./main_startup_check.vue";
function tuneAudio() {
sendTestFrame();
}
function set_audio_level() {
saveSettingsToFile();
setTxAudioLevel(settings.tx_audio_level);
}
function deleteChat() { function deleteChat() {
//console.log(chat.selectedCallsign) //console.log(chat.selectedCallsign)
@ -121,6 +112,8 @@ const transmissionSpeedChartDataMessageInfo = computed(() => ({
</script> </script>
<template> <template>
<main_startup_check />
<!-- updater release notes--> <!-- updater release notes-->
<div <div
class="modal fade" class="modal fade"
@ -927,7 +920,7 @@ const transmissionSpeedChartDataMessageInfo = computed(() => ({
<button <button
type="button" type="button"
class="btn btn-sm btn-outline-secondary" class="btn btn-sm btn-outline-secondary"
@click="tuneAudio" @click="sendModemTestFrame()"
> >
Tune Tune
</button> </button>
@ -1183,30 +1176,53 @@ const transmissionSpeedChartDataMessageInfo = computed(() => ({
></button> ></button>
</div> </div>
<div class="modal-body"> <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"> <div class="input-group input-group-sm mb-1">
<span class="input-group-text">Test-Frame</span> <span class="input-group-text">Test-Frame</span>
<button <button
type="button" type="button"
id="sendTestFrame" id="sendTestFrame"
@click="sendTestFrame()" @click="sendModemTestFrame()"
class="btn btn-danger" class="btn btn-danger"
> >
Transmit Transmit
</button> </button>
</div> </div>
<div class="input-group input-group-sm mb-1"> <div class="input-group input-group-sm mb-1">
<span class="input-group-text">TX Level</span> <span class="input-group-text">RX Level</span>
<span class="input-group-text">{{ settings.tx_audio_level }}</span> <span class="input-group-text">{{
settings.remote.AUDIO.rx_audio_level
}}</span>
<span class="input-group-text w-75"> <span class="input-group-text w-75">
<input <input
type="range" type="range"
class="form-range" class="form-range"
min="0" min="-30"
max="250" 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" step="1"
id="audioLevelTX" id="audioLevelTX"
@click="set_audio_level()" @change="onChange"
v-model="settings.tx_audio_level" v-model.number="settings.remote.AUDIO.tx_audio_level"
/></span> /></span>
</div> </div>
</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"> <script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler";
import { setActivePinia } from "pinia"; import { setActivePinia } from "pinia";
import pinia from "../store/index"; import pinia from "../store/index";
setActivePinia(pinia); setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js"; import { settingsStore as settings } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
function saveSettings() {
saveSettingsToFile();
}
</script> </script>
<template> <template>
@ -62,15 +55,13 @@ function saveSettings() {
maxlength="8" maxlength="8"
aria-label="Input group" aria-label="Input group"
aria-describedby="btnGroupAddon" aria-describedby="btnGroupAddon"
v-model="settings.mycall" v-model="settings.remote.STATION.mycall"
@input="saveSettings"
/> />
<select <select
class="form-select form-select-sm" class="form-select form-select-sm"
aria-label=".form-select-sm" aria-label=".form-select-sm"
id="myCallSSID" id="myCallSSID"
v-model="settings.myssid" v-model="settings.remote.STATION.myssid"
@change="saveSettings"
> >
<option selected value="0">0</option> <option selected value="0">0</option>
<option value="1">1</option> <option value="1">1</option>
@ -112,8 +103,7 @@ function saveSettings() {
maxlength="6" maxlength="6"
aria-label="Input group" aria-label="Input group"
aria-describedby="btnGroupAddon" aria-describedby="btnGroupAddon"
v-model="settings.mygrid" v-model="settings.remote.STATION.mygrid"
@input="saveSettings"
/> />
</div> </div>
</div> </div>

View file

@ -1,15 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler";
import { startRigctld, stopRigctld } from "../js/daemon";
import { setActivePinia } from "pinia"; import { setActivePinia } from "pinia";
import pinia from "../store/index"; import pinia from "../store/index";
setActivePinia(pinia); setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js"; import { settingsStore as settings} from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { useStateStore } from "../store/stateStore.js"; import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia); const state = useStateStore(pinia);
@ -18,15 +14,15 @@ function startStopRigctld() {
switch (state.rigctld_started) { switch (state.rigctld_started) {
case "stopped": 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; break;
case "running": case "running":
stopRigctld(); //stopRigctld();
// dirty hack for calling this command twice, otherwise modem won't stop rigctld from time to time // dirty hack for calling this command twice, otherwise modem won't stop rigctld from time to time
stopRigctld(); //stopRigctld();
break; break;
default: default:
} }
@ -36,19 +32,19 @@ function selectRadioControl() {
// @ts-expect-error // @ts-expect-error
switch (event.target.id) { switch (event.target.id) {
case "list-rig-control-none-list": case "list-rig-control-none-list":
settings.radiocontrol = "disabled"; settings.remote.RADIO.control = "disabled";
break; break;
case "list-rig-control-rigctld-list": case "list-rig-control-rigctld-list":
settings.radiocontrol = "rigctld"; settings.remote.RADIO.control = "rigctld";
break; break;
case "list-rig-control-tci-list": case "list-rig-control-tci-list":
settings.radiocontrol = "tci"; settings.remote.RADIO.control = "tci";
break; break;
default: default:
console.log("default=!=="); console.log("default=!==");
settings.radiocontrol = "disabled"; settings.remote.RADIO.control = "disabled";
} }
saveSettingsToFile(); //saveSettingsToFile();
} }
@ -61,8 +57,9 @@ alert("not yet implemented")
</script> </script>
<template> <template>
<div class="card mb-0"> <div class="mb-3">
<div class="card-header p-1"> <div class="card mb-1">
<div class="card-header p-1">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-1"> <div class="col-1">
@ -85,7 +82,7 @@ alert("not yet implemented")
href="#list-rig-control-none" href="#list-rig-control-none"
role="tab" role="tab"
aria-controls="list-rig-control-none" aria-controls="list-rig-control-none"
v-bind:class="{ active: settings.radiocontrol === 'disabled' }" v-bind:class="{ active: settings.remote.RADIO.control === 'disabled' }"
@click="selectRadioControl()" @click="selectRadioControl()"
>None</a >None</a
> >
@ -96,7 +93,7 @@ alert("not yet implemented")
href="#list-rig-control-rigctld" href="#list-rig-control-rigctld"
role="tab" role="tab"
aria-controls="list-rig-control-rigctld" aria-controls="list-rig-control-rigctld"
v-bind:class="{ active: settings.radiocontrol === 'rigctld' }" v-bind:class="{ active: settings.remote.RADIO.control === 'rigctld' }"
@click="selectRadioControl()" @click="selectRadioControl()"
>Rigctld</a >Rigctld</a
> >
@ -107,7 +104,7 @@ alert("not yet implemented")
href="#list-rig-control-tci" href="#list-rig-control-tci"
role="tab" role="tab"
aria-controls="list-rig-control-tci" aria-controls="list-rig-control-tci"
v-bind:class="{ active: settings.radiocontrol === 'tci' }" v-bind:class="{ active: settings.remote.RADIO.control === 'tci' }"
@click="selectRadioControl()" @click="selectRadioControl()"
>TCI</a >TCI</a
> >
@ -132,7 +129,7 @@ alert("not yet implemented")
<div class="tab-content" id="rig-control-nav-tabContent"> <div class="tab-content" id="rig-control-nav-tabContent">
<div <div
class="tab-pane fade" 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" id="list-rig-control-none"
role="tabpanel" role="tabpanel"
aria-labelledby="list-rig-control-none-list" aria-labelledby="list-rig-control-none-list"
@ -146,27 +143,11 @@ alert("not yet implemented")
<div <div
class="tab-pane fade" class="tab-pane fade"
id="list-rig-control-rigctld" 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" role="tabpanel"
aria-labelledby="list-rig-control-rigctld-list" 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">
<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"> <div class="input-group input-group-sm mb-1">
<span class="input-group-text">Rigctld service</span> <span class="input-group-text">Rigctld service</span>
@ -217,7 +198,7 @@ alert("not yet implemented")
<div <div
class="tab-pane fade" class="tab-pane fade"
id="list-rig-control-tci" 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" role="tabpanel"
aria-labelledby="list-rig-control-tci-list" aria-labelledby="list-rig-control-tci-list"
> >
@ -231,7 +212,7 @@ alert("not yet implemented")
placeholder="tci IP" placeholder="tci IP"
id="tci_ip" id="tci_ip"
aria-label="Device IP" aria-label="Device IP"
v-model="settings.tci_ip" v-model="settings.remote.TCI.tci_ip"
/> />
</div> </div>
@ -243,7 +224,7 @@ alert("not yet implemented")
placeholder="tci port" placeholder="tci port"
id="tci_port" id="tci_port"
aria-label="Device Port" aria-label="Device Port"
v-model="settings.tci_port" v-model="settings.remote.TCI.tci_port"
/> />
</div> </div>
</div> </div>
@ -275,4 +256,5 @@ alert("not yet implemented")
</div> </div>
--> -->
</div> </div>
</div>
</template> </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"> <script setup lang="ts">
import { setActivePinia } from "pinia"; import { setActivePinia } from "pinia";
import pinia from "../store/index"; import pinia from "../store/index";
setActivePinia(pinia); setActivePinia(pinia);
@ -7,101 +6,21 @@ setActivePinia(pinia);
import { useStateStore } from "../store/stateStore.js"; import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia); const state = useStateStore(pinia);
import { useSettingsStore } from "../store/settingsStore.js"; import { settingsStore as settings } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { useAudioStore } from "../store/audioStore.js";
const audioStore = useAudioStore(pinia);
import { startModem, stopModem } from "../js/daemon";
import { saveSettingsToFile } from "../js/settingsHandler";
function startStopModem() {
switch (state.modem_running_state) {
case "stopped":
let startupInputDeviceValue = parseInt((<HTMLSelectElement>document.getElementById("audio_input_selectbox")).value);
let startupOutputDeviceValue = parseInt((<HTMLSelectElement>document.getElementById("audio_output_selectbox")).value);
let startupInputDeviceIndex = (<HTMLSelectElement>document.getElementById("audio_input_selectbox")).selectedIndex;
let startupOutputDeviceIndex = (<HTMLSelectElement>document.getElementById("audio_output_selectbox")).selectedIndex;
audioStore.startupInputDevice = startupInputDeviceValue
audioStore.startupOutputDevice = startupOutputDeviceValue
// get full name of audio device
settings.rx_audio = (<HTMLSelectElement>document.getElementById("audio_input_selectbox")).options[startupInputDeviceIndex].text;
settings.tx_audio = (<HTMLSelectElement>document.getElementById("audio_output_selectbox")).options[startupOutputDeviceIndex].text;
saveSettingsToFile();
startModem();
break;
case "running":
stopModem();
break;
default:
}
}
</script> </script>
<template> <template>
<nav class="navbar bg-body-tertiary border-bottom"> <nav class="navbar bg-body-tertiary border-bottom">
<div class="mx-auto"> <div class="mx-auto">
<span class="badge bg-secondary me-4" <span class="badge bg-secondary me-4">
>Modem location | {{ settings.modem_host }}</span Modem Connection {{ state.modem_connection }}
> </span>
<span class="badge bg-secondary me-4">
<span class="badge bg-secondary me-4" Modem {{ state.is_modem_running }}
>Service | {{ state.modem_running_state }}</span </span>
> <span class="badge bg-secondary me-4">
RIG Control {{ state.rigctld_started }}
<div class="btn-group" role="group"></div> </span>
<div class="btn-group me-4" role="group">
<button
type="button"
id="startModem"
class="btn btn-sm btn-outline-success"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="false"
title="Start the Modem. Please set your audio and radio settings first!"
@click="startStopModem()"
v-bind:class="{ disabled: state.modem_running_state === 'running' }"
>
<i class="bi bi-play-fill"></i>
<span class="ms-2">start modem</span>
</button>
<button
type="button"
id="stopModem"
class="btn btn-sm btn-outline-danger"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
data-bs-html="false"
title="Stop the Modem."
@click="startStopModem()"
v-bind:class="{ disabled: state.modem_running_state === 'stopped' }"
>
<i class="bi bi-stop-fill"></i>
<span class="ms-2">stop modem</span>
</button>
</div>
<button
type="button"
id="openHelpModalStartStopModem"
data-bs-toggle="modal"
data-bs-target="#startStopModemHelpModal"
class="btn me-4 p-0 border-0"
>
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
</button>
</div> </div>
</nav>
</nav>
</template> </template>

View file

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

View file

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

View file

@ -1,58 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler"; import { setConfig } from "../js/api";
import { setActivePinia } from "pinia"; import { setActivePinia } from "pinia";
import pinia from "../store/index"; import pinia from "../store/index";
setActivePinia(pinia); setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js"; import { settingsStore as settings } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
function saveSettings() {
saveSettingsToFile();
}
</script> </script>
<template> <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"> <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">Enable MESH protocol</label>
<label class="input-group-text w-50"> <label class="input-group-text w-50">
@ -61,10 +17,8 @@ function saveSettings() {
class="form-check-input" class="form-check-input"
type="checkbox" type="checkbox"
id="enableMeshSwitch" id="enableMeshSwitch"
@change="saveSettings" @change="setConfig"
v-model="settings.enable_mesh_features" v-model="settings.remote.MESH.enable_protocol"
true-value="True"
false-value="False"
/> />
<label class="form-check-label" for="enableMeshSwitch" <label class="form-check-label" for="enableMeshSwitch"
>experimental! REALLY!</label >experimental! REALLY!</label
@ -72,24 +26,6 @@ function saveSettings() {
</div> </div>
</label> </label>
</div> </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="center">
<div class="badge text-bg-danger"> <div class="badge text-bg-danger">
<i class="bi bi-shield-exclamation"></i> These options may not work and <i class="bi bi-shield-exclamation"></i> These options may not work and

View file

@ -1,65 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler"; import { setColormap } from "../js/waterfallHandler";
import { setActivePinia } from "pinia"; import { setActivePinia } from "pinia";
import pinia from "../store/index"; import pinia from "../store/index";
setActivePinia(pinia); setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js"; import { settingsStore as settings } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
function saveSettings() { function saveSettings() {
saveSettingsToFile(); //saveSettingsToFile();
setColormap();
} }
</script> </script>
<template> <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"> <div class="input-group input-group-sm mb-1">
<span class="input-group-text w-50">Waterfall theme</span> <span class="input-group-text w-50">Waterfall theme</span>
<select <select
class="form-select form-select-sm w-50" class="form-select form-select-sm w-50"
id="wftheme_selector" id="wftheme_selector"
@change="saveSettings" @change="saveSettings"
v-model="settings.wftheme" v-model="settings.local.wf_theme"
disabled
> >
<option value="2">Default</option> <option value="2">Default</option>
<option value="0">Turbo</option> <option value="0">Turbo</option>
@ -70,49 +30,17 @@ function saveSettings() {
<option value="6">Binary</option> <option value="6">Binary</option>
</select> </select>
</div> </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"> <div class="input-group input-group-sm mb-1">
<span class="input-group-text w-50">Update channel</span> <span class="input-group-text w-50">Update channel</span>
<select <select
class="form-select form-select-sm w-50" class="form-select form-select-sm w-50"
id="update_channel_selector" id="update_channel_selector"
@change="saveSettings" @change="saveSettings"
v-model="settings.update_channel" v-model="settings.local.update_channel"
disabled
> >
<option value="latest">stable</option> <option value="latest">Stable</option>
<option value="beta">beta</option> <option value="beta">Beta</option>
<option value="alpha">alpha</option> <option value="alpha">Alpha</option>
</select> </select>
</div> </div>
<div class="input-group input-group-sm mb-1"> <div class="input-group input-group-sm mb-1">
@ -124,10 +52,7 @@ function saveSettings() {
type="checkbox" type="checkbox"
id="NotificationSwitch" id="NotificationSwitch"
@change="saveSettings" @change="saveSettings"
v-model="settings.enable_sys_notification" v-model="settings.local.enable_sys_notification"
true-value="True"
false-value="False"
disabled
/> />
<label class="form-check-label" for="NotificationSwitch" <label class="form-check-label" for="NotificationSwitch"
>Show system pop-ups</label >Show system pop-ups</label
@ -135,24 +60,4 @@ function saveSettings() {
</div> </div>
</label> </label>
</div> </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> </template>

View file

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

View file

@ -1,53 +1,141 @@
<script setup lang="ts"> <script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler"; import { settingsStore as settings, onChange } from "../store/settingsStore.js";
import { setActivePinia } from "pinia";
import pinia from "../store/index"; import pinia from "../store/index";
setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js"; import { useStateStore } from "../store/stateStore.js";
const settings = useSettingsStore(pinia); const state = useStateStore(pinia);
function saveSettings() { import { startModem, stopModem } from "../js/api.js";
saveSettingsToFile(); import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
}
</script> </script>
<template> <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"> <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 <input
type="text" type="number"
class="form-control" class="form-control"
placeholder="modem port" placeholder="modem port"
id="modem_port" id="modem_port"
maxlength="5" maxlength="5"
max="65534" max="65534"
min="1025" min="1025"
@change="saveSettings" v-model.number="settings.local.port"
v-model="settings.modem_port"
/> />
</div> </div>
<div class="input-group input-group-sm mb-1"> <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 <input
type="text" type="text"
class="form-control" class="form-control"
placeholder="modem host" placeholder="modem host"
id="modem_port" id="modem_port"
@change="saveSettings" v-model="settings.local.host"
v-model="settings.modem_host"
/> />
</div> </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"> <div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">TX delay in ms</label> <label class="input-group-text w-50">TX delay in ms</label>
<select <select
class="form-select form-select-sm" class="form-select form-select-sm"
id="tx_delay" id="tx_delay"
@change="saveSettings" @change="onChange"
v-model="settings.tx_delay" v-model.number="settings.remote.MODEM.tx_delay"
> >
<option value="0">0</option> <option value="0">0</option>
<option value="50">50</option> <option value="50">50</option>
@ -79,27 +167,27 @@ function saveSettings() {
<select <select
class="form-select form-select-sm" class="form-select form-select-sm"
id="tuning_range_fmin" id="tuning_range_fmin"
@change="saveSettings" @change="onChange"
v-model="settings.tuning_range_fmin" v-model.number="settings.remote.MODEM.tuning_range_fmin"
> >
<option value="-50.0">-50.0</option> <option value="-50">-50</option>
<option value="-100.0">-100.0</option> <option value="-100">-100</option>
<option value="-150.0">-150.0</option> <option value="-150">-150</option>
<option value="-200.0">-200.0</option> <option value="-200">-200</option>
<option value="-250.0">-250.0</option> <option value="-250">-250</option>
</select> </select>
<label class="input-group-text">fmax</label> <label class="input-group-text">fmax</label>
<select <select
class="form-select form-select-sm" class="form-select form-select-sm"
id="tuning_range_fmax" id="tuning_range_fmax"
@change="saveSettings" @change="onChange"
v-model="settings.tuning_range_fmax" v-model.number="settings.remote.MODEM.tuning_range_fmax"
> >
<option value="50.0">50.0</option> <option value="50">50</option>
<option value="100.0">100.0</option> <option value="100">100</option>
<option value="150.0">150.0</option> <option value="150">150</option>
<option value="200.0">200.0</option> <option value="200">200</option>
<option value="250.0">250.0</option> <option value="250">250</option>
</select> </select>
</div> </div>
<div class="input-group input-group-sm mb-1"> <div class="input-group input-group-sm mb-1">
@ -109,8 +197,8 @@ function saveSettings() {
aria-label=".form-select-sm" aria-label=".form-select-sm"
id="beaconInterval" id="beaconInterval"
style="width: 6rem" style="width: 6rem"
@change="saveSettings" @change="onChange"
v-model="settings.beacon_interval" v-model.number="settings.remote.MODEM.beacon_interval"
> >
<option value="60">60 secs</option> <option value="60">60 secs</option>
<option value="90">90 secs</option> <option value="90">90 secs</option>
@ -122,40 +210,7 @@ function saveSettings() {
<option value="3600">60 mins</option> <option value="3600">60 mins</option>
</select> </select>
</div> </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"> <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">Enable 250Hz bandwidth mode</label>
<label class="input-group-text w-50"> <label class="input-group-text w-50">
@ -164,10 +219,8 @@ function saveSettings() {
class="form-check-input" class="form-check-input"
type="checkbox" type="checkbox"
id="250HzModeSwitch" id="250HzModeSwitch"
v-model="settings.low_bandwidth_mode" v-model="settings.remote.MODEM.enable_low_bandwidth_mode"
true-value="True" @change="onChange"
false-value="False"
@change="saveSettings"
/> />
<label class="form-check-label" for="250HzModeSwitch">250Hz</label> <label class="form-check-label" for="250HzModeSwitch">250Hz</label>
</div> </div>
@ -181,10 +234,8 @@ function saveSettings() {
class="form-check-input" class="form-check-input"
type="checkbox" type="checkbox"
id="respondCQSwitch" id="respondCQSwitch"
v-model="settings.respond_to_cq" v-model="settings.remote.MODEM.respond_to_cq"
true-value="True" @change="onChange"
false-value="False"
@change="saveSettings"
/> />
<label class="form-check-label" for="respondCQSwitch">QRV</label> <label class="form-check-label" for="respondCQSwitch">QRV</label>
</div> </div>
@ -196,8 +247,8 @@ function saveSettings() {
<select <select
class="form-select form-select-sm" class="form-select form-select-sm"
id="rx_buffer_size" id="rx_buffer_size"
@change="saveSettings" @change="onChange"
v-model="settings.rx_buffer_size" v-model.number="settings.remote.MODEM.rx_buffer_size"
> >
<option value="1">1</option> <option value="1">1</option>
<option value="2">2</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"> <script setup lang="ts">
import { saveSettingsToFile } from "../js/settingsHandler"; import { setConfig } from "../js/api";
import { setActivePinia } from "pinia"; import { setActivePinia } from "pinia";
import pinia from "../store/index"; import pinia from "../store/index";
setActivePinia(pinia); setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js"; import { settingsStore as settings, onChange } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
function saveSettings() {
saveSettingsToFile();
}
</script> </script>
<template> <template>
@ -22,10 +17,8 @@ function saveSettings() {
class="form-check-input" class="form-check-input"
type="checkbox" type="checkbox"
id="ExplorerSwitch" id="ExplorerSwitch"
@change="saveSettings" @change="onChange"
v-model="settings.enable_explorer" v-model="settings.remote.STATION.enable_explorer"
true-value="True"
false-value="False"
/> />
<label class="form-check-label" for="ExplorerSwitch">Publish</label> <label class="form-check-label" for="ExplorerSwitch">Publish</label>
</div> </div>
@ -39,10 +32,8 @@ function saveSettings() {
class="form-check-input" class="form-check-input"
type="checkbox" type="checkbox"
id="ExplorerStatsSwitch" id="ExplorerStatsSwitch"
@change="saveSettings" @change="onChange"
v-model="settings.explorer_stats" v-model="settings.remote.STATION.enable_stats"
true-value="True"
false-value="False"
/> />
<label class="form-check-label" for="ExplorerStatsSwitch" <label class="form-check-label" for="ExplorerStatsSwitch"
>Publish stats</label >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"; import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia); const state = useStateStore(pinia);
import { useSettingsStore } from "../store/settingsStore.js"; import { settingsStore as settings } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { sendMessage, sendBroadcastChannel } from "./sock.js";
import { displayToast } from "./popupHandler.js"; import { displayToast } from "./popupHandler.js";
//const FD = require("./src/js/freedata.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 // define default message object
interface Attachment { interface Attachment {
@ -67,6 +74,13 @@ interface beaconDefaultObject {
snr: string; snr: string;
} }
interface newChatDefaultObject {
command: string;
is_new: boolean;
timestamp: number;
dxcallsign: string;
}
// ---- MessageDB // ---- MessageDB
try { try {
var PouchDB = require("pouchdb"); var PouchDB = require("pouchdb");
@ -179,7 +193,7 @@ export function newBroadcast(broadcastChannel, chatmessage) {
}; };
//sendMessage(newChatObj) //sendMessage(newChatObj)
sendBroadcastChannel(newChatObj); //sendBroadcastChannel(newChatObj);
addObjToDatabase(newChatObj); addObjToDatabase(newChatObj);
} }
@ -279,16 +293,6 @@ function sortChatList() {
return reorderedData; 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) { export function getMessageAttachment(id) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
@ -395,11 +399,11 @@ async function dbClean() {
//Too slow on older/slower machines //Too slow on older/slower machines
//await db.compact(); //await db.compact();
let message = "Database maintenance is complete"; let message = "Database maintenance is complete, ";
displayToast("info", "bi bi-info-circle", message, 5000); //displayToast("info", "bi bi-info-circle", message, 5000);
message = "Removed " + itemCount + " items from database"; message += "removed " + itemCount + " items from database";
displayToast("info", "bi bi-info-circle", message, 5000); console.log(message);
} }
// function to update transmission status // function to update transmission status
@ -419,7 +423,7 @@ export function updateUnsortedChatListEntry(uuid, object, value) {
var data = getFromUnsortedChatListByUUID(uuid); var data = getFromUnsortedChatListByUUID(uuid);
if (data) { if (data) {
data[object] = value; data[object] = value;
console.log("Entry updated:", data[object]); //console.log("Entry updated:", data[object]);
chat.sorted_chat_list = sortChatList(); chat.sorted_chat_list = sortChatList();
return data; 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 return null; // Return null if not found
} }
@ -452,22 +456,27 @@ export function getNewMessagesByDXCallsign(dxcallsign): [number, number, any] {
let new_counter = 0; let new_counter = 0;
let total_counter = 0; let total_counter = 0;
let item_array = []; let item_array = [];
if ( try {
typeof dxcallsign !== "undefined" && if (
typeof chat.sorted_chat_list[dxcallsign] !== "undefined" 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]) for (const key in chat.sorted_chat_list[dxcallsign]) {
//item_array.push(chat.sorted_chat_list[dxcallsign][key]) //console.log(chat.sorted_chat_list[dxcallsign][key])
if (chat.sorted_chat_list[dxcallsign][key].is_new) { //item_array.push(chat.sorted_chat_list[dxcallsign][key])
item_array.push(chat.sorted_chat_list[dxcallsign][key]); if (chat.sorted_chat_list[dxcallsign][key].is_new) {
new_counter += 1; 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) { export function resetIsNewMessage(uuid, value) {
@ -485,7 +494,7 @@ export function databaseUpsert(id, object, value) {
}) })
.then(function (res) { .then(function (res) {
// success, res is {rev: '1-xxx', updated: true, id: 'myDocId'} // success, res is {rev: '1-xxx', updated: true, id: 'myDocId'}
console.log(res); //console.log(res);
}) })
.catch(function (err) { .catch(function (err) {
// error // error
@ -592,7 +601,7 @@ function addObjToDatabase(newobj) {
console.log("new database entry"); console.log("new database entry");
console.log(response); console.log(response);
if (newobj.command === "msg") { if (newobj.command === "msg" || newobj.command === "newchat") {
chat.unsorted_chat_list.push(newobj); chat.unsorted_chat_list.push(newobj);
chat.sorted_chat_list = sortChatList(); chat.sorted_chat_list = sortChatList();
} }
@ -692,6 +701,31 @@ function deleteFromDatabaseByCallsign(callsign) {
console.log(err); 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 // function for handling a received beacon
export function newBeaconReceived(obj) { export function newBeaconReceived(obj) {
@ -746,11 +780,11 @@ export function newBeaconReceived(obj) {
}); });
// check if auto retry enabled // check if auto retry enabled
console.log("-----------------------------------------"); //console.log("-----------------------------------------");
console.log(settings.enable_auto_retry.toUpperCase()); //console.log(settings.enable_auto_retry.toUpperCase());
if (settings.enable_auto_retry.toUpperCase() == "TRUE") { //if (settings.enable_auto_retry.toUpperCase() == "TRUE") {
checkForWaitingMessages(dxcallsign); // checkForWaitingMessages(dxcallsign);
} //}
} }
// function for handling a received message // function for handling a received message
@ -830,27 +864,39 @@ export function newMessageReceived(message, protocol) {
*/ */
console.log(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 = { let newChatObj: messageDefaultObject = {
command: "msg", command: "msg",
hmac_signed: protocol["hmac_signed"], hmac_signed: false,
percent: 100, percent: 100,
bytesperminute: protocol["bytesperminute"], bytesperminute: 0,
is_new: true, is_new: true,
_id: message[3], _id: message[3],
timestamp: message[4], timestamp: message[4],
dxcallsign: protocol["dxcallsign"], dxcallsign: protocol["dxcall"],
dxgrid: protocol["dxgrid"], dxgrid: "",
msg: message[5], msg: message[5],
checksum: message[2], checksum: message[2],
type: protocol["status"], type: "received",
status: protocol["status"], status: "received",
attempt: 1, attempt: 1,
uuid: message[3], uuid: message[3],
duration: protocol["duration"], duration: 0,
nacks: protocol["nacks"], nacks: 0,
speed_list: protocol["speed_list"], speed_list: "[]",
_attachments: { _attachments: {
[message[6]]: { [message[6]]: {
content_type: message[7], content_type: message[7],
@ -1029,13 +1075,12 @@ async function checkForWaitingMessages(dxcall) {
// this ensures, we are only sending one message at once // this ensures, we are only sending one message at once
// @ts-expect-error // @ts-expect-error
console.log(result.docs[0]); console.log(result.docs[0]);
console.log( //console.log(
"attempt: " + // "attempt: " +
// @ts-expect-error // result.docs[0].attempt +
result.docs[0].attempt + // "/" +
"/" + // settings.max_retry_attempts,
settings.max_retry_attempts, //);
);
// @ts-expect-error // @ts-expect-error
if (result.docs[0].attempt < settings.max_retry_attempts) { if (result.docs[0].attempt < settings.max_retry_attempts) {
console.log("repeating message..."); console.log("repeating message...");
@ -1049,3 +1094,42 @@ async function checkForWaitingMessages(dxcall) {
console.log(err); 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"); 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 { addDataToWaterfall } from "../js/waterfallHandler.js";
import { import {
@ -18,8 +18,7 @@ setActivePinia(pinia);
import { useStateStore } from "../store/stateStore.js"; import { useStateStore } from "../store/stateStore.js";
const stateStore = useStateStore(pinia); const stateStore = useStateStore(pinia);
import { useSettingsStore } from "../store/settingsStore.js"; import { settingsStore as settings } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
var client = new net.Socket(); var client = new net.Socket();
var socketchunk = ""; // Current message, per connection. var socketchunk = ""; // Current message, per connection.
@ -31,8 +30,10 @@ const split_char = "0;1;";
// global to keep track of Modem connection error emissions // global to keep track of Modem connection error emissions
var modemShowConnectStateError = 1; var modemShowConnectStateError = 1;
var setTxAudioLevelOnce = true; var setTxAudioLevelOnce = true;
var setRxAudioLevelOnce = true;
// network connection Timeout // network connection Timeout
setTimeout(connectModem, 2000); //setTimeout(connectModem, 2000);
function connectModem() { function connectModem() {
//exports.connectModem = function(){ //exports.connectModem = function(){
@ -171,7 +172,8 @@ client.on("data", function (socketdata) {
stateStore.arq_state = data["arq_state"]; stateStore.arq_state = data["arq_state"];
stateStore.mode = data["mode"]; stateStore.mode = data["mode"];
stateStore.bandwidth = data["bandwidth"]; 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 // if audio level is different from config one, send new audio level to modem
//console.log(parseInt(stateStore.tx_audio_level)) //console.log(parseInt(stateStore.tx_audio_level))
//console.log(parseInt(settings.tx_audio_level)) //console.log(parseInt(settings.tx_audio_level))
@ -185,6 +187,16 @@ client.on("data", function (socketdata) {
setTxAudioLevel(settings.tx_audio_level); 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.dbfs_level = data["audio_dbfs"];
stateStore.ptt_state = data["ptt_state"]; stateStore.ptt_state = data["ptt_state"];
stateStore.speed_level = data["speed_level"]; stateStore.speed_level = data["speed_level"];
@ -215,7 +227,9 @@ client.on("data", function (socketdata) {
stateStore.dbfs_level = Math.round(stateStore.dbfs_level); stateStore.dbfs_level = Math.round(stateStore.dbfs_level);
stateStore.arq_total_bytes = data["total_bytes"]; 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.dxcallsign = data["dxcallsign"];
stateStore.beacon_state = data["beacon_state"]; stateStore.beacon_state = data["beacon_state"];
@ -541,6 +555,11 @@ export function setTxAudioLevel(value) {
'{"type" : "set", "command" : "tx_audio_level", "value" : "' + value + '"}'; '{"type" : "set", "command" : "tx_audio_level", "value" : "' + value + '"}';
writeTncCommand(command); writeTncCommand(command);
} }
export function setRxAudioLevel(value) {
var command =
'{"type" : "set", "command" : "rx_audio_level", "value" : "' + value + '"}';
writeTncCommand(command);
}
// Send Message // Send Message
export function sendMessage(obj) { export function sendMessage(obj) {
@ -700,11 +719,6 @@ function sendResponseSharedFile(dxcallsign, sharedFile, sharedFileData) {
sendResponse(dxcallsign, 255, 1, sharedFile + "/" + sharedFileData, "res-2"); 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 // Get RX BUffer
export function getRxBuffer() { export function getRxBuffer() {
@ -713,21 +727,6 @@ export function getRxBuffer() {
writeTncCommand(command); 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 // OPEN ARQ SESSION
export function connectARQ(dxcallsign) { export function connectARQ(dxcallsign) {
var command = 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) { //exports.atob = function (data) {
return window.btoa(Buffer.from(data, "base64").toString("utf8")); 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 path from "node:path";
import fs from "fs"; import fs from "fs";
import { setColormap } from "./waterfallHandler";
// pinia store setup // pinia store setup
import { setActivePinia } from "pinia"; import { setActivePinia } from "pinia";
import pinia from "../store/index"; import pinia from "../store/index";
setActivePinia(pinia); setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js"; import { settingsStore as settings, onChange } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
import { useStateStore } from "../store/stateStore";
const stateStore = useStateStore(pinia);
// --------------------------------- // ---------------------------------
console.log(process.env); console.log(process.env);
@ -47,68 +50,18 @@ if (!fs.existsSync(configFolder)) {
} }
// create config file if not exists with defaults // create config file if not exists with defaults
const configDefaultSettings = const configDefaultSettings = `{
'{\ "modem_host": "127.0.0.1",
"modem_host": "127.0.0.1",\ "modem_port": 5000,
"modem_port": 3000,\ "spectrum": "waterfall",
"daemon_host": "127.0.0.1",\ "theme": "default",
"daemon_port": 3001,\ "screen_height": 430,
"rx_audio" : "",\ "screen_width": 1050,
"tx_audio" : "",\ "update_channel": "latest",
"tx_audio_level" : 100,\ "wftheme": 2,
"mycall": "AA0AA-0",\ "enable_sys_notification": 1
"myssid": "0",\ }`;
"mygrid": "JN40aa",\ var parsedConfig = JSON.parse(configDefaultSettings);
"radiocontrol" : "disabled",\
"hamlib_deviceid": 6,\
"hamlib_deviceport": "ignore",\
"hamlib_stop_bits": "ignore",\
"hamlib_data_bits": "ignore",\
"hamlib_handshake": "ignore",\
"hamlib_serialspeed": "ignore",\
"hamlib_dtrstate": "ignore",\
"hamlib_pttprotocol": "ignore",\
"hamlib_ptt_port": "ignore",\
"hamlib_dcd": "ignore",\
"hamlbib_serialspeed_ptt": 9600,\
"hamlib_rigctld_port" : 4532,\
"hamlib_rigctld_ip" : "127.0.0.1",\
"hamlib_rigctld_path" : "",\
"hamlib_rigctld_server_port" : 4532,\
"hamlib_rigctld_custom_args": "",\
"tci_port" : 50001,\
"tci_ip" : "127.0.0.1",\
"spectrum": "waterfall",\
"enable_scatter" : "False",\
"enable_fft" : "False",\
"enable_fsk" : "False",\
"low_bandwidth_mode" : "False",\
"theme" : "default",\
"screen_height" : 430,\
"screen_width" : 1050,\
"update_channel" : "latest",\
"beacon_interval" : 300,\
"received_files_folder" : "None",\
"tuning_range_fmin" : "-50.0",\
"tuning_range_fmax" : "50.0",\
"respond_to_cq" : "True",\
"rx_buffer_size" : 16, \
"enable_explorer" : "False", \
"wftheme": 2, \
"high_graphics" : "True",\
"explorer_stats" : "False", \
"auto_tune" : "False", \
"enable_is_writing" : "True", \
"shared_folder_path" : ".", \
"enable_request_profile" : "True", \
"enable_request_shared_folder" : "False", \
"max_retry_attempts" : 5, \
"enable_auto_retry" : "False", \
"tx_delay" : 0, \
"auto_start": 0, \
"enable_sys_notification": 1, \
"enable_mesh_features": "False" \
}';
if (!fs.existsSync(configPath)) { if (!fs.existsSync(configPath)) {
fs.writeFileSync(configPath, configDefaultSettings); fs.writeFileSync(configPath, configDefaultSettings);
@ -123,7 +76,6 @@ export function loadSettings() {
// if parameter not exists, add it to running config to prevent errors // if parameter not exists, add it to running config to prevent errors
console.log("CONFIG VALIDATION ----------------------------- "); console.log("CONFIG VALIDATION ----------------------------- ");
var parsedConfig = JSON.parse(configDefaultSettings);
for (var key in parsedConfig) { for (var key in parsedConfig) {
if (config.hasOwnProperty(key)) { if (config.hasOwnProperty(key)) {
console.log("FOUND SETTTING [" + key + "]: " + config[key]); console.log("FOUND SETTTING [" + key + "]: " + config[key]);
@ -133,9 +85,12 @@ export function loadSettings() {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
} }
try { try {
if (key == "wftheme") {
setColormap();
}
if (key == "mycall") { if (key == "mycall") {
settings.mycall = config[key].split("-")[0]; settings.remote.STATION.mycall = config[key].split("-")[0];
settings.myssid = config[key].split("-")[1]; settings.remote.STATION.myssid = config[key].split("-")[1];
} else { } else {
settings[key] = config[key]; settings[key] = config[key];
} }
@ -145,9 +100,46 @@ export function loadSettings() {
} }
} }
export function saveSettingsToFile() { //No longer used...
console.log("save settings to file..."); //export function saveSettingsToFile() {
let config = settings.getJSON(); // console.log("save settings to file...");
console.log(config); // let config = settings.getJSON();
fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); // 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"; import pinia from "../store/index";
setActivePinia(pinia); setActivePinia(pinia);
import { useSettingsStore } from "../store/settingsStore.js"; import { settingsStore as settings } from "../store/settingsStore.js";
const settings = useSettingsStore(pinia);
var spectrum = new Object(); var spectrum = new Object();
var spectrums = [];
export function initWaterfall() { export function initWaterfall(id) {
spectrum = new Spectrum("waterfall", { spectrum = new Spectrum(id, {
spectrumPercent: 0, 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,
}); });
spectrum.setColorMap(settings.local.wf_theme);
console.log(settings.wftheme); spectrums.push(spectrum);
spectrum.setColorMap(settings.wftheme); return spectrum;
} }
export function addDataToWaterfall(data) { export function addDataToWaterfall(data) {
//console.log(spectrum) data = JSON.parse(data);
try { if (data.constructor !== Array) return;
spectrum.addData(data); spectrums.forEach((element) => {
} catch (e) { //console.log(element);
//console.log(e); 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 { createApp } from "vue";
import { createPinia } from "pinia"; import { createPinia } from "pinia";
import { loadSettings } from "./js/settingsHandler";
import "./styles.css"; import "./styles.css";
import { Chart, Filler } from "chart.js"; import { Chart, Filler } from "chart.js";
// Register the Filler plugin globally // Register the Filler plugin globally
@ -30,9 +29,11 @@ const tooltipList = [...tooltipTriggerList].map(
(tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl), (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' getRemote().then(() => {
import "./js/daemon"; initConnections();
import "./js/sock.js"; getModemState();
//import './js/settingsHandler.js' });

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 inputText = ref("");
var inputFile = ref(); var inputFile = ref();
var inputFileName = ref(); var inputFileName = ref("-");
var inputFileType = ref(); var inputFileType = ref("-");
var inputFileSize = ref(); var inputFileSize = ref("-");
var callsign_list = ref(); var callsign_list = ref();
var sorted_chat_list = ref(); var sorted_chat_list = ref();

View file

@ -1,231 +1,126 @@
import { defineStore } from "pinia"; import { reactive, ref, watch } from "vue";
import { ref } from "vue";
export const useSettingsStore = defineStore("settingsStore", () => { import { getConfig, setConfig } from "../js/api";
// audio
var tx_audio = ref();
var rx_audio = ref();
var tx_audio_level = ref();
// network var nconf = require("nconf");
var modem_host = ref("127.0.0.1"); nconf.file({ file: "config/config.json" });
var modem_port = ref(3000);
var daemon_host = ref(modem_host.value);
var daemon_port = ref(modem_port.value + 1);
// app // +++
var screen_height = ref(430); //GUI DEFAULT SETTINGS........
var screen_width = ref(1050); //Set GUI defaults here, they will be used if not found in config/config.json
var theme = ref("default"); //They should be an exact mirror (variable wise) of settingsStore.local
var wftheme = ref(2); //Nothing else should be needed aslong as components are using v-bind
var high_graphics = ref("False"); // +++
var auto_start = ref(0); nconf.defaults({
var enable_sys_notification = ref(1); local: {
host: "127.0.0.1",
// chat port: "5000",
var shared_folder_path = ref("."); spectrum: "waterfall",
var enable_request_profile = ref("True"); wf_theme: 2,
var enable_request_shared_folder = ref("False"); update_channel: "alpha",
var max_retry_attempts = ref(5); enable_sys_notification: false,
var enable_auto_retry = ref("False"); grid_layout: "[]",
grid_preset: "[]",
// station grid_enabled: true,
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,
};
}); });
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", () => { export const useStateStore = defineStore("stateStore", () => {
var busy_state = ref("-"); var busy_state = ref("-");
var arq_state = ref("-"); var arq_state = ref("-");
var frequency = ref("-"); var frequency = ref(0);
var new_frequency = ref(0); var new_frequency = ref(14093000);
var mode = ref("-"); var mode = ref("-");
var rf_level = ref("10"); var rf_level = ref("10");
var bandwidth = ref("-"); var bandwidth = ref("-");
var dbfs_level_percent = ref(0); var dbfs_level_percent = ref(0);
var dbfs_level = 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 speed_level = ref(0);
var fft = ref(); var fft = ref();
var channel_busy = ref(""); var channel_busy = ref(false);
var channel_busy_slot = ref(); var channel_busy_slot = ref([false, false, false, false, false]);
var scatter = ref(); var scatter = ref([]);
var s_meter_strength_percent = ref(0); var s_meter_strength_percent = ref(0);
var s_meter_strength_raw = ref(0); var s_meter_strength_raw = ref(0);
var modem_connection = ref("disconnected"); var modem_connection = ref("disconnected");
var modemStartCount = ref(0); var modemStartCount = ref(0);
var modem_running_state = ref("--------"); var is_modem_running = ref();
var arq_total_bytes = ref(0); var arq_total_bytes = ref(0);
var arq_transmission_percent = ref(0); var arq_transmission_percent = ref(0);
var heard_stations = ref(""); var activities = ref([]);
var heard_stations = ref([]);
var dxcallsign = ref(""); var dxcallsign = ref("");
var arq_session_state = ref(""); var arq_session_state = ref("");
var arq_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 hamlib_status = ref("");
var tx_audio_level = ref(""); var tx_audio_level = ref("");
var rx_audio_level = ref("");
var alc = ref(""); var alc = ref("");
var is_codec2_traffic = ref(""); var is_codec2_traffic = ref("");
@ -61,78 +65,12 @@ export const useStateStore = defineStore("stateStore", () => {
var rx_buffer_length = ref(); var rx_buffer_length = ref();
function getChannelBusySlotState(slot) {
const slot_state = channel_busy_slot.value;
if (typeof slot_state !== "undefined") {
// Replace 'False' with 'false' to match JavaScript's boolean representation
const string = slot_state
.replace(/False/g, "false")
.replace(/True/g, "true");
// Parse the string to get an array
const arr = JSON.parse(string);
return arr[slot];
} else {
// Handle the undefined case
return false;
}
}
function updateTncState(state) { function updateTncState(state) {
modem_connection.value = state; modem_connection.value = state;
if (modem_connection.value == "open") { 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 //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++; 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, fft,
channel_busy, channel_busy,
channel_busy_slot, channel_busy_slot,
getChannelBusySlotState,
scatter, scatter,
ptt_state, ptt_state,
s_meter_strength_percent, s_meter_strength_percent,
@ -159,6 +96,7 @@ export const useStateStore = defineStore("stateStore", () => {
audio_recording, audio_recording,
hamlib_status, hamlib_status,
tx_audio_level, tx_audio_level,
rx_audio_level,
alc, alc,
updateTncState, updateTncState,
arq_transmission_percent, arq_transmission_percent,
@ -168,10 +106,12 @@ export const useStateStore = defineStore("stateStore", () => {
arq_seconds_until_finish, arq_seconds_until_finish,
arq_seconds_until_timeout, arq_seconds_until_timeout,
arq_seconds_until_timeout_percent, arq_seconds_until_timeout_percent,
modem_running_state, modem_connection,
is_modem_running,
arq_session_state, arq_session_state,
is_codec2_traffic, is_codec2_traffic,
rf_level, rf_level,
activities,
heard_stations, heard_stations,
beacon_state, beacon_state,
rigctld_started, rigctld_started,
@ -179,5 +119,6 @@ export const useStateStore = defineStore("stateStore", () => {
python_version, python_version,
modem_version, modem_version,
rx_buffer_length, 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; const sourcemap = isServe || !!process.env.VSCODE_DEBUG;
return { return {
optimizeDeps: {
esbuildOptions: {
target: "esnext",
},
},
build: {
target: "esnext",
},
plugins: [ plugins: [
vue(), vue(),
electron([ 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 atexit
import multiprocessing import multiprocessing
import crcengine import crcengine
import sounddevice as sd import sounddevice as sd
import structlog import structlog
import numpy as np
import queue
import threading
atexit.register(sd._terminate) atexit.register(sd._terminate)
@ -28,8 +30,8 @@ def get_audio_devices():
# we need to reset and initialize sounddevice before running the multiprocessing part. # 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 # If we are not doing this at this early point, not all devices will be displayed
sd._terminate() #sd._terminate()
sd._initialize() #sd._initialize()
# log.debug("[AUD] get_audio_devices") # log.debug("[AUD] get_audio_devices")
with multiprocessing.Manager() as manager: with multiprocessing.Manager() as manager:
@ -49,15 +51,10 @@ def get_audio_devices():
def device_crc(device) -> str: def device_crc(device) -> str:
crc_hwid = crc_algorithm(bytes(f"{device['name']}.{device['hostapi']}", encoding="utf-8"))
crc_hwid = crc_algorithm(bytes(f"{device}", encoding="utf-8"))
crc_hwid = crc_hwid.to_bytes(2, byteorder="big") crc_hwid = crc_hwid.to_bytes(2, byteorder="big")
crc_hwid = crc_hwid.hex() crc_hwid = crc_hwid.hex()
return crc_hwid
hostapi_name = sd.query_hostapis(device['hostapi'])['name']
return f"{device['name']} [{hostapi_name}] [{crc_hwid}]"
def fetch_audio_devices(input_devices, output_devices): def fetch_audio_devices(input_devices, output_devices):
""" """
@ -91,13 +88,243 @@ def fetch_audio_devices(input_devices, output_devices):
max_output_channels = 0 max_output_channels = 0
if max_input_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 # check if device not in device list
if new_input_device not in input_devices: if new_input_device not in input_devices:
input_devices.append(new_input_device) input_devices.append(new_input_device)
if max_output_channels > 0: 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 # check if device not in device list
if new_output_device not in output_devices: if new_output_device not in output_devices:
output_devices.append(new_output_device) 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 Enumeration for codec2 modes and names
""" """
sig0 = 19 signalling = 19
sig1 = 19
datac0 = 14 datac0 = 14
datac1 = 10 datac1 = 10
datac3 = 12 datac3 = 12
datac4 = 18 datac4 = 18
datac13 = 19 datac13 = 19
fsk_ldpc = 9
fsk_ldpc_0 = 200
fsk_ldpc_1 = 201
class FREEDV_MODE_USED_SLOTS(Enum): class FREEDV_MODE_USED_SLOTS(Enum):
@ -105,13 +101,14 @@ for file in files:
#log.info("[C2 ] Libcodec2 loaded", path=file) #log.info("[C2 ] Libcodec2 loaded", path=file)
break break
except OSError as err: 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 # Quit module if codec2 cant be loaded
if api is None or "api" not in locals(): if api is None or "api" not in locals():
log.critical("[C2 ] Error: Libcodec2 not loaded - Exiting") log.critical("[C2 ] Error: Libcodec2 not loaded - Exiting")
sys.exit(1) sys.exit(1)
log.info("[C2 ] Libcodec2 loaded...")
# ctypes function init # ctypes function init
# api.freedv_set_tuning_range.restype = ctypes.c_int # api.freedv_set_tuning_range.restype = ctypes.c_int
@ -425,3 +422,46 @@ class resampler:
self.filter_mem8 = in8_mem[: self.MEM8] self.filter_mem8 = in8_mem[: self.MEM8]
return out48 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