Merge branch 'develop' into dependabot/npm_and_yarn/gui/develop/vueuse/electron-10.7.1

This commit is contained in:
DJ2LS 2024-01-15 16:56:48 +01:00 committed by GitHub
commit e40007c755
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 1654 additions and 1255 deletions

View file

@ -11,17 +11,17 @@ jobs:
os: [ubuntu-20.04, macos-latest, windows-latest] os: [ubuntu-20.04, macos-latest, windows-latest]
include: include:
- os: ubuntu-20.04 - os: ubuntu-20.04
zip_name: ubuntu_modem zip_name: freedata-server_ubuntu
generator: Unix Makefiles generator: Unix Makefiles
modem_binary_name: freedata-server modem_binary_name: freedata-server
- os: macos-latest - os: macos-latest
zip_name: macos_modem zip_name: freedata-server_macos
generator: Unix Makefiles generator: Unix Makefiles
modem_binary_name: freedata-server modem_binary_name: freedata-server
- os: windows-latest - os: windows-latest
zip_name: windows_modem zip_name: freedata-server_windows
generator: Visual Studio 16 2019 generator: Visual Studio 16 2019
modem_binary_name: freedata-server.exe modem_binary_name: freedata-server.exe
@ -74,25 +74,25 @@ jobs:
- name: Build binaries - name: Build binaries
working-directory: modem working-directory: modem
run: | run: |
python3 -m nuitka --enable-plugin=numpy --remove-output --assume-yes-for-downloads --standalone server.py 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 #- name: Download Portaudio binaries Linux macOS
if: ${{!startsWith(matrix.os, 'windows')}} # if: ${{!startsWith(matrix.os, 'windows')}}
working-directory: modem # working-directory: modem
run: | # run: |
if ! test -d "server.dist/modem/_sounddevice_data"; then # if ! test -d "server.dist/modem/_sounddevice_data"; then
git clone https://github.com/spatialaudio/portaudio-binaries dist/modem/_sounddevice_data/portaudio-binaries # git clone https://github.com/spatialaudio/portaudio-binaries dist/modem/_sounddevice_data/portaudio-binaries
fi # fi
- name: Download Portaudio binaries Windows #- name: Download Portaudio binaries Windows
if: ${{startsWith(matrix.os, 'windows')}} # if: ${{startsWith(matrix.os, 'windows')}}
working-directory: modem # working-directory: modem
run: | # run: |
if(Test-Path -Path "server.dist/modem/_sounddevice_data"){ # if(Test-Path -Path "server.dist/modem/_sounddevice_data"){
echo "sounddevice folder already exists" # echo "sounddevice folder already exists"
} else { # } else {
git clone https://github.com/spatialaudio/portaudio-binaries dist/modem/_sounddevice_data/portaudio-binaries # git clone https://github.com/spatialaudio/portaudio-binaries dist/modem/_sounddevice_data/portaudio-binaries
} # }
- name: LIST ALL FILES - name: LIST ALL FILES
run: ls -R run: ls -R
@ -113,6 +113,15 @@ jobs:
path: . path: .
# exclusions: '*.git* /*node_modules/* .editorconfig' # 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 - name: Release Modem
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')

View file

@ -4,8 +4,6 @@
"port": "5000", "port": "5000",
"spectrum": "waterfall", "spectrum": "waterfall",
"wf_theme": 2, "wf_theme": 2,
"theme": "default_light",
"high_graphics": true,
"update_channel": "alpha", "update_channel": "alpha",
"enable_sys_notification": false "enable_sys_notification": false
} }

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

@ -50,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-")) {

View file

@ -8,7 +8,7 @@ import "../../node_modules/gridstack/dist/gridstack.min.css";
import { GridStack } from "gridstack"; import { GridStack } from "gridstack";
import { useStateStore } from "../store/stateStore.js"; import { useStateStore } from "../store/stateStore.js";
const state = useStateStore(pinia); const state = useStateStore(pinia);
import { setModemFrequency } from "../js/api"; import { setRadioParameters } from "../js/api";
import { saveLocalSettingsToConfig, settingsStore } from "../store/settingsStore"; import { saveLocalSettingsToConfig, settingsStore } from "../store/settingsStore";
import active_heard_stations from "./grid/grid_active_heard_stations.vue"; import active_heard_stations from "./grid/grid_active_heard_stations.vue";
@ -25,14 +25,20 @@ import grid_button from "./grid/button.vue";
import grid_ptt from "./grid/grid_ptt.vue"; import grid_ptt from "./grid/grid_ptt.vue";
import grid_mycall from "./grid/grid_mycall.vue"; import grid_mycall from "./grid/grid_mycall.vue";
import grid_stop from "./grid/grid_stop.vue"; import grid_stop from "./grid/grid_stop.vue";
import grid_tune from "./grid/grid_tune.vue";
import grid_CQ_btn from "./grid/grid_CQ.vue"; import grid_CQ_btn from "./grid/grid_CQ.vue";
import grid_ping from "./grid/grid_ping.vue"; import grid_ping from "./grid/grid_ping.vue";
import grid_freq from "./grid/grid_frequency.vue"; import grid_freq from "./grid/grid_frequency.vue";
import grid_beacon from "./grid/grid_beacon.vue";
import grid_mycall_small from "./grid/grid_mycall small.vue";
import grid_scatter from "./grid/grid_scatter.vue";
import { stateDispatcher } from "../js/eventHandler"; import { stateDispatcher } from "../js/eventHandler";
import { Scatter } from "vue-chartjs";
let count = ref(0); let count = ref(0);
let grid = null; // DO NOT use ref(null) as proxies GS will break all logic when comparing structures... see https://github.com/gridstack/gridstack.js/issues/2115 let grid = null; // DO NOT use ref(null) as proxies GS will break all logic when comparing structures... see https://github.com/gridstack/gridstack.js/issues/2115
let items = ref([]); let items = ref([]);
let gridEnabledLocal = ref(true);
class gridWidget { class gridWidget {
//Contains the vue component //Contains the vue component
component2; component2;
@ -46,32 +52,46 @@ class gridWidget {
autoPlace; autoPlace;
//Category to place widget in widget picker //Category to place widget in widget picker
category; category;
constructor(component, size, text, quickfill, autoPlace, category) { //Unique ID for widget
id;
constructor(component, size, text, quickfill, autoPlace, category, id) {
this.component2 = component; this.component2 = component;
this.size = size; this.size = size;
this.text = text; this.text = text;
this.quickFill = quickfill; this.quickFill = quickfill;
this.autoPlace = autoPlace; this.autoPlace = autoPlace;
this.category = category; this.category = category;
this.id = id;
} }
} }
//Array of grid widgets, do not change array order as it'll affect saved configs //Array of grid widgets, do not change array order as it'll affect saved configs
const gridWidgets = [ const gridWidgets = [
new gridWidget( new gridWidget(
active_heard_stations, grid_activities,
{ x: 0, y: 0, w: 16, h: 40 }, { x: 0, y: 53, w: 6, h: 55 },
"Detailed heard stations list", "Activities list",
true, true,
true, true,
"Activity", "Activity",
8,
),
new gridWidget(
active_heard_stations,
{ x: 0, y: 13, w: 16, h: 40 },
"Heard stations list (detailed)",
true,
true,
"Activity",
0,
), ),
new gridWidget( new gridWidget(
active_stats, active_stats,
{ x: 16, y: 16, w: 8, h: 80 }, { x: 16, y: 16, w: 8, h: 72 },
"Stats (waterfall, etc)", "Stats (waterfall, etc)",
true, true,
true, true,
"Stats", "Stats",
1,
), ),
new gridWidget( new gridWidget(
active_audio_level, active_audio_level,
@ -80,94 +100,7 @@ const gridWidgets = [
false, false,
true, true,
"Audio", "Audio",
), 2,
new gridWidget(
active_rig_control,
{ x: 6, y: 40, w: 9, h: 15 },
"Rig control main",
false,
true,
"Rig",
),
new gridWidget(
active_broadcasts,
{ x: 6, y: 70, w: 6, h: 15 },
"Broadcasts main (horizontal)",
false,
true,
"Broadcasts",
),
new gridWidget(
mini_heard_stations,
{ x: 1, y: 1, w: 6, h: 54 },
"Mini heard stations list",
false,
true,
"Activity",
),
new gridWidget(
s_meter,
{ x: 16, y: 0, w: 4, h: 8 },
"S-Meter",
true,
true,
"Rig",
),
new gridWidget(
dbfs_meter,
{ x: 20, y: 0, w: 4, h: 8 },
"Dbfs Meter",
true,
true,
"Audio",
),
new gridWidget(
grid_activities,
{ x: 0, y: 40, w: 6, h: 55 },
"Activities list",
true,
true,
"Activity",
),
new gridWidget(
active_broadcasts_vert,
{ x: 9, y: 55, w: 10, h: 40 },
"Broadcasts main (vertical)",
true,
true,
"Broadcasts",
),
new gridWidget(
grid_ptt,
{ x: 17, y: 8, w: 5, h: 12 },
"Tx/PTT indicator",
true,
true,
"Rig",
),
new gridWidget(
grid_mycall,
{ x: 8, y: 40, w: 5, h: 15 },
"My callsign widget",
true,
true,
"Other",
),
new gridWidget(
grid_CQ_btn,
{ x: 3, y: 27, w: 2, h: 8 },
"CQ Button",
false,
true,
"Broadcasts",
),
new gridWidget(
grid_ping,
{ x: 3, y: 27, w: 4, h: 9 },
"Ping Widget",
false,
true,
"Broadcasts",
), ),
new gridWidget( new gridWidget(
grid_freq, grid_freq,
@ -176,26 +109,178 @@ const gridWidgets = [
true, true,
true, true,
"Rig", "Rig",
14,
), ),
new gridWidget( new gridWidget(
grid_stop, active_rig_control,
{ x: 8, y: 40, w: 5, h: 15 }, { x: 6, y: 40, w: 9, h: 15 },
"Stop Widget", "Rig control main",
false,
true,
"Rig",
3,
),
new gridWidget(
grid_beacon,
{ x: 3, y: 27, w: 3, h: 8 },
"Beacon button",
false,
true,
"Broadcasts",
16,
),
new gridWidget(
active_broadcasts,
{ x: 6, y: 70, w: 6, h: 15 },
"Broadcasts main (horizontal)",
false,
true,
"Broadcasts",
4,
),
new gridWidget(
mini_heard_stations,
{ x: 1, y: 1, w: 6, h: 54 },
"Heard stations list (small)",
false,
true,
"Activity",
5,
),
new gridWidget(
s_meter,
{ x: 16, y: 0, w: 4, h: 8 },
"S-Meter",
true,
true,
"Rig",
6,
),
new gridWidget(
dbfs_meter,
{ x: 20, y: 0, w: 4, h: 8 },
"Dbfs meter",
true,
true,
"Audio",
7,
),
new gridWidget(
active_broadcasts_vert,
{ x: 6, y: 53, w: 10, h: 35 },
"Broadcasts main (vertical)",
true,
true,
"Broadcasts",
9,
),
new gridWidget(
grid_ptt,
{ x: 2, y: 0, w: 5, h: 13 },
"Tx/PTT indicator",
true,
true,
"Rig",
10,
),
new gridWidget(
grid_mycall,
{ x: 7, y: 0, w: 9, h: 13 },
"My callsign widget",
true, true,
true, true,
"Other", "Other",
11,
), ),
new gridWidget(
grid_mycall_small,
{ x: 8, y: 40, w: 4, h: 8 },
"My callsign widget (small)",
false,
true,
"Other",
17,
),
new gridWidget(
grid_CQ_btn,
{ x: 3, y: 27, w: 2, h: 8 },
"CQ button",
false,
true,
"Broadcasts",
12,
),
new gridWidget(
grid_ping,
{ x: 3, y: 27, w: 4, h: 9 },
"Ping widget",
false,
true,
"Broadcasts",
13,
),
new gridWidget(
grid_stop,
{ x: 0, y: 0, w: 2, h: 13 },
"Stop widget",
true,
true,
"Other",
15,
),
new gridWidget(
grid_tune,
{ x: 16, y: 8, w: 2, h: 8 },
"Tune widget",
true,
true,
"Audio",
18,
),
new gridWidget(
grid_scatter,
{ x: 0, y: 114, w: 6, h: 30 },
"Scatter graph",
false,
true,
"Stats",
19,
),
//New new widget ID should be 20
]; ];
function updateFrequencyAndApply(frequency) { function updateFrequencyAndApply(frequency) {
state.new_frequency = frequency; state.new_frequency = frequency;
setModemFrequency(state.new_frequency); set_radio_parameters();
} }
function set_hamlib_frequency_manually() { function set_radio_parameters(){
setModemFrequency(state.new_frequency); setRadioParameters(state.new_frequency, state.mode, state.rf_level);
} }
function savePreset()
{
settingsStore.local.grid_preset=settingsStore.local.grid_layout;
console.log("Saved grid preset")
}
function loadPreset()
{
clearAllItems();
settingsStore.local.grid_layout=settingsStore.local.grid_preset;
restoreGridLayoutFromConfig();
console.log("Restored grid preset")
}
onMounted(() => { onMounted(() => {
grid = GridStack.init({ grid = GridStack.init({
// DO NOT use grid.value = GridStack.init(), see above // DO NOT use grid.value = GridStack.init(), see above
@ -290,13 +375,17 @@ function onChange(event, changeItems) {
function restoreGridLayoutFromConfig(){ function restoreGridLayoutFromConfig(){
//Try to load grid from saved config //Try to load grid from saved config
//On mounted seems to be called multiple times; so check to make sure items is empty first //On mounted seems to be called multiple times; so check to make sure items is empty first
//array format: 0 = x, 1 = y, 2 = w, 3 = h, 4 = gridwidget index //array format: 0 = x, 1 = y, 2 = w, 3 = h, 4 = gridwidget ID
if (items.value.length == 0){ if (items.value.length == 0){
let savedGrid = JSON.parse(settingsStore.local.grid_layout); let savedGrid = JSON.parse(settingsStore.local.grid_layout);
if (savedGrid.length > 0 ) console.info("Restoring " + savedGrid.length + " widget(s) from config"); if (savedGrid.length > 0 ) console.info("Restoring " + savedGrid.length + " widget(s) from config");
for (let i=0; i < savedGrid.length;i++ ){ for (let i=0; i < savedGrid.length;i++ ){
//Find widget by ID
var widgetIndex = gridWidgets.findIndex((gw) => gw.id == savedGrid[i][4])
//Refs are passed, so grab original settings for restoration //Refs are passed, so grab original settings for restoration
let tempGW = gridWidgets[parseInt(savedGrid[i][4])]; //let tempGW = gridWidgets[parseInt(savedGrid[i][4])];
let tempGW = gridWidgets[widgetIndex];
let backupGWsize = tempGW.size; let backupGWsize = tempGW.size;
tempGW.autoPlace=false; tempGW.autoPlace=false;
tempGW.size={x:savedGrid[i][0], y:savedGrid[i][1], w:savedGrid[i][2], h:savedGrid[i][3]} tempGW.size={x:savedGrid[i][0], y:savedGrid[i][1], w:savedGrid[i][2], h:savedGrid[i][3]}
@ -312,7 +401,11 @@ function saveGridLayout()
let cfg = []; let cfg = [];
for (let i=0; items.value.length > i; i++) { for (let i=0; items.value.length > i; i++) {
var widget = gridWidgets.findIndex((gw) => gw.component2.__name == items.value[i].component2.__name) var widget = gridWidgets.findIndex((gw) => gw.component2.__name == items.value[i].component2.__name)
cfg[i] = [items.value[i].x, items.value[i].y, items.value[i].w,items.value[i].h, widget ]; //Get the widget's id to store in config
var widgetid = gridWidgets[widget].id;
//Debug code to return index of widget based on id
//console.log(widgetid + "-" + widget);
cfg[i] = [items.value[i].x, items.value[i].y, items.value[i].w,items.value[i].h, widgetid ];
} }
settingsStore.local.grid_layout=JSON.stringify(cfg); settingsStore.local.grid_layout=JSON.stringify(cfg);
saveLocalSettingsToConfig(); saveLocalSettingsToConfig();
@ -339,7 +432,13 @@ function remove(widget) {
grid.removeWidget(selector, false); grid.removeWidget(selector, false);
saveGridLayout(); saveGridLayout();
} }
function disableGrid() {
gridEnabledLocal.value = !gridEnabledLocal.value
if (gridEnabledLocal.value)
grid.enable();
else
grid.disable();
}
function clearAllItems() { function clearAllItems() {
grid.removeAll(false); grid.removeAll(false);
count.value = 0; count.value = 0;
@ -388,6 +487,7 @@ function quickfill() {
<button <button
@click="remove(w)" @click="remove(w)"
class="btn-close grid-stack-floaty-btn" class="btn-close grid-stack-floaty-btn"
:class="gridEnabledLocal === true ? 'visible':'invisible'"
></button> ></button>
<component :is="w.component2" /> <component :is="w.component2" />
</div> </div>
@ -405,7 +505,14 @@ function quickfill() {
> >
<div class="offcanvas-header"> <div class="offcanvas-header">
<h5 class="offcanvas-title" id="offcanvasGridItemsLabel"> <h5 class="offcanvas-title" id="offcanvasGridItemsLabel">
Manage grid widgets Manage grid widgets &nbsp;<button
class="btn btn-sm btn-outline-info"
type="button"
@click="disableGrid"
title="Lock/unloack changes to grid"
>
<i class="bi" :class="gridEnabledLocal == true ? 'bi-unlock-fill' : 'bi-lock-fill'"></i>
</button>&nbsp;
</h5> </h5>
<button <button
@ -580,9 +687,28 @@ function quickfill() {
class="btn btn-sm btn-outline-warning" class="btn btn-sm btn-outline-warning"
type="button" type="button"
@click="clearAllItems" @click="clearAllItems"
title="Clear all items from the grid"
> >
Clear grid Clear grid
</button> </button>
<hr/>
<button
class="btn btn-sm btn-outline-dark"
type="button"
@click="loadPreset"
title="Restore your saved grid preset (clears current grid)"
>
Restore preset
</button>&nbsp;
<button
class="btn btn-sm btn-outline-dark"
type="button"
@click="savePreset"
title="Save current grid layout as a preset that can be restored using restore preset button"
>
Save preset
</button>
</div> </div>
</div> </div>
@ -627,7 +753,7 @@ function quickfill() {
<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',
}" }"

View file

@ -6,7 +6,10 @@ function emitClick() {
} }
</script> </script>
<template> <template>
<button class="btn btn-small btn-outline-secondary" v-on:click="emitClick"> <button
class="btn btn-small btn-outline-secondary mb-1"
v-on:click="emitClick"
>
{{ btnText }} {{ btnText }}
</button> </button>
</template> </template>

View file

@ -8,28 +8,9 @@ const state = useStateStore(pinia);
</script> </script>
<template> <template>
<div class="card w-100 h-100"> <div class="card w-100 h-100">
<div class="card-header p-0"> <div class="card-header p-0 mb-1">
<div class="dropdown">
<button
class="btn btn-sm btn-outline-secondary dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
></button>
<i class="bi bi-volume-up" style="font-size: 1rem"></i>&nbsp; <i class="bi bi-volume-up" style="font-size: 1rem"></i>&nbsp;
<strong>Audio</strong> <strong>Audio</strong>
<ul class="dropdown-menu">
<li>
<a
class="dropdown-item"
data-bs-toggle="modal"
data-bs-target="#audioModal"
href="#"
>Tune</a
>
</li>
</ul>
</div>
</div> </div>
<div class="card-body pt-0 pb-0"> <div class="card-body pt-0 pb-0">
<div class="container-wide"> <div class="container-wide">

View file

@ -1,18 +1,14 @@
<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";
import { setModemRigMode, setModemRigPowerLvl } from "../../js/api"; import { setRadioParameters } from "../../js/api";
setActivePinia(pinia); setActivePinia(pinia);
import { useStateStore } from "../../store/stateStore.js"; import { useStateStore } from "../../store/stateStore.js";
const state = useStateStore(pinia); const state = useStateStore(pinia);
function set_hamlib_mode() { function set_radio_parameters() {
setModemRigMode(state.mode); setRadioParameters(state.frequency, state.mode, state.rf_level);
}
function set_hamlib_rf_level() {
setModemRigPowerLvl(state.rf_level);
} }
</script> </script>
@ -51,7 +47,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',
}" }"
@ -73,7 +69,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

@ -332,10 +332,7 @@ onMounted(() => {
role="stats_tabpanel" role="stats_tabpanel"
aria-labelledby="list-waterfall-list" aria-labelledby="list-waterfall-list"
> >
<canvas <canvas v-bind:id="localSpectrumID" class="force-gpu"></canvas>
v-bind:id="localSpectrumID"
:class="settings.local.high_graphics == true ? 'force-gpu' : ''"
></canvas>
</div> </div>
<div <div
class="tab-pane fade h-100 w-100" class="tab-pane fade h-100 w-100"

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,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

@ -6,19 +6,15 @@ import pinia from "../../store/index";
setActivePinia(pinia); setActivePinia(pinia);
import { settingsStore } from "../../store/settingsStore.js"; import { settingsStore } from "../../store/settingsStore.js";
function updateMyCall() {
setConfig();
}
</script> </script>
<template> <template>
<div <div
class="d-flex justify-content-center align-items-center object-fill border rounded w-100 h-100" class="d-flex justify-content-center align-items-center object-fill border rounded w-100 h-100"
> >
<h1> <h2>
{{ settingsStore.remote.STATION.mycall }}-{{ {{ settingsStore.remote.STATION.mycall }}-{{
settingsStore.remote.STATION.myssid settingsStore.remote.STATION.myssid
}} }}
</h1> </h2>
</div> </div>
</template> </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

@ -1,7 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref } from "vue";
import { setActivePinia } from "pinia"; import { setActivePinia } from "pinia";
import { setConfig } from "../../js/api";
import pinia from "../../store/index"; import pinia from "../../store/index";
setActivePinia(pinia); setActivePinia(pinia);
@ -13,11 +11,8 @@ function stopAllTransmissions() {
} }
</script> </script>
<template> <template>
<div <a
class="d-flex justify-content-center align-items-center object-fill rounded w-100 h-100" class="btn btn-outline-danger d-flex border justify-content-center align-items-center object-fill rounded w-100 h-100"
>
<button
class="btn border btn-outline-danger list-group-item w-100 h-100"
id="stop_transmission_connection" id="stop_transmission_connection"
data-bs-toggle="tooltip" data-bs-toggle="tooltip"
data-bs-trigger="hover" data-bs-trigger="hover"
@ -26,6 +21,5 @@ function stopAllTransmissions() {
title="Abort session and stop transmissions" title="Abort session and stop transmissions"
> >
<i class="bi bi-sign-stop-fill h1"></i> <i class="bi bi-sign-stop-fill h1"></i>
</button> </a>
</div>
</template> </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

@ -18,7 +18,7 @@ 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 main_modem_healthcheck from "./main_modem_healthcheck.vue";
import Dynamic_components2 from "./dynamic_components2.vue"; import Dynamic_components from "./dynamic_components.vue";
import { stopTransmission } from "../js/api"; import { stopTransmission } from "../js/api";
@ -321,7 +321,7 @@ function stopAllTransmissions() {
role="tabpanel" role="tabpanel"
aria-labelledby="list-grid-list" aria-labelledby="list-grid-list"
> >
<Dynamic_components2 /> <Dynamic_components />
</div> </div>
<div <div

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 { setModemRigMode, setModemRigPowerLvl } from "../js/api"; 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() {
setModemRigMode(state.mode);
}
function set_hamlib_rf_level() {
setModemRigPowerLvl(state.rf_level);
} }
</script> </script>
@ -102,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',
}" }"
@ -215,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',
}" }"
@ -237,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

@ -368,7 +368,7 @@ export default {
width: 100%; width: 100%;
height: 200px; height: 200px;
" "
:class="settings.local.high_graphics == true ? 'force-gpu' : ''" class="force-gpu'"
></canvas> ></canvas>
</div> </div>
<div <div

View file

@ -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

@ -8,7 +8,7 @@ setActivePinia(pinia);
import { useChatStore } from "../store/chatStore.js"; import { useChatStore } from "../store/chatStore.js";
const chat = useChatStore(pinia); const chat = useChatStore(pinia);
import { settingsStore as settings } from "../store/settingsStore.js"; import { settingsStore as settings, onChange } from "../store/settingsStore.js";
import { sendModemTestFrame } from "../js/api"; import { sendModemTestFrame } from "../js/api";
@ -19,16 +19,6 @@ import {
import main_startup_check from "./main_startup_check.vue"; import main_startup_check from "./main_startup_check.vue";
function set_tx_audio_level() {
saveSettingsToFile();
setTxAudioLevel(settings.tx_audio_level);
}
function set_rx_audio_level() {
saveSettingsToFile();
setRxAudioLevel(settings.rx_audio_level);
}
function deleteChat() { function deleteChat() {
//console.log(chat.selectedCallsign) //console.log(chat.selectedCallsign)
deleteChatByCallsign(chat.selectedCallsign); deleteChatByCallsign(chat.selectedCallsign);
@ -1186,6 +1176,10 @@ 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
@ -1199,7 +1193,9 @@ const transmissionSpeedChartDataMessageInfo = computed(() => ({
</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">RX Level</span> <span class="input-group-text">RX Level</span>
<span class="input-group-text">{{ settings.rx_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"
@ -1208,13 +1204,15 @@ const transmissionSpeedChartDataMessageInfo = computed(() => ({
max="20" max="20"
step="1" step="1"
id="audioLevelRX" id="audioLevelRX"
@click="set_rx_audio_level()" @change="onChange"
v-model="settings.rx_audio_level" v-model.number="settings.remote.AUDIO.rx_audio_level"
/></span> /></span>
</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">TX Level</span>
<span class="input-group-text">{{ settings.tx_audio_level }}</span> <span class="input-group-text">{{
settings.remote.AUDIO.tx_audio_level
}}</span>
<span class="input-group-text w-75"> <span class="input-group-text w-75">
<input <input
type="range" type="range"
@ -1223,8 +1221,8 @@ const transmissionSpeedChartDataMessageInfo = computed(() => ({
max="20" max="20"
step="1" step="1"
id="audioLevelTX" id="audioLevelTX"
@click="set_tx_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

@ -3,7 +3,6 @@ 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_rigcontrol from "./settings_rigcontrol.vue"; import settings_rigcontrol from "./settings_rigcontrol.vue";
import settings_hamlib from "./settings_hamlib.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";
@ -76,20 +75,7 @@ import settings_exp from "./settings_exp.vue";
Rig Control Rig Control
</button> </button>
</li> </li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
id="hamlib-tab"
data-bs-toggle="tab"
data-bs-target="#hamlib"
type="button"
role="tab"
aria-controls="profile"
aria-selected="false"
>
Hamlib
</button>
</li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button <button
class="nav-link" class="nav-link"
@ -134,7 +120,10 @@ 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--> <!-- Station tab contents-->
@ -180,15 +169,6 @@ import settings_exp from "./settings_exp.vue";
<settings_rigcontrol /> <settings_rigcontrol />
</div> </div>
<div
class="tab-pane"
id="hamlib"
role="tabpanel"
aria-labelledby="hamlib-tab"
tabindex="0"
>
<settings_hamlib />
</div>
<div <div
class="tab-pane" class="tab-pane"
id="modem" id="modem"

View file

@ -26,24 +26,6 @@ import { settingsStore as settings } from "../store/settingsStore.js";
</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

@ -13,44 +13,6 @@ function saveSettings() {
} }
</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.local.theme"
>
<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
@ -68,34 +30,6 @@ 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.local.high_graphics"
/>
<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

View file

@ -42,268 +42,288 @@ import { serialDeviceOptions } from "../js/deviceFormHelper";
v-model.number="settings.remote.RADIO.model_id" v-model.number="settings.remote.RADIO.model_id"
> >
<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>
<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> </select>
</div> </div>

View file

@ -94,7 +94,41 @@ import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
</option> </option>
</select> </select>
</div> </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

View file

@ -1,5 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { settingsStore as settings, onChange } from "../store/settingsStore.js"; import { settingsStore as settings, onChange } from "../store/settingsStore.js";
import settings_hamlib from "./settings_hamlib.vue";
import settings_tci from "./settings_tci.vue";
</script> </script>
<template> <template>
@ -23,29 +25,54 @@ import { settingsStore as settings, onChange } from "../store/settingsStore.js";
<hr class="m-2" /> <hr class="m-2" />
<div class="input-group input-group-sm mb-1"> <nav>
<span class="input-group-text" style="width: 180px">TCI IP Address</span> <div class="nav nav-tabs" id="nav-tab" role="tablist">
<input <button
type="text" class="nav-link active"
class="form-control" id="nav-home-tab"
placeholder="TCI IP" data-bs-toggle="tab"
id="rigcontrol_tci_ip" data-bs-target="#nav-hamlib"
aria-label="Device IP" type="button"
@change="onChange" role="tab"
v-model="settings.remote.TCI.tci_ip" 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> </div>
<div class="input-group input-group-sm mb-1"> <hr class="m-2" />
<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> </template>

View file

@ -1,5 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { settingsStore as settings, onChange } from "../store/settingsStore.js"; 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> </script>
<template> <template>
<!-- station callsign --> <!-- station callsign -->
@ -14,8 +29,8 @@ import { settingsStore as settings, onChange } from "../store/settingsStore.js";
id="myCall" id="myCall"
aria-label="Station Callsign" aria-label="Station Callsign"
aria-describedby="basic-addon1" aria-describedby="basic-addon1"
@change="onChange"
v-model="settings.remote.STATION.mycall" v-model="settings.remote.STATION.mycall"
@change="validateCall"
/> />
</div> </div>

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

@ -117,12 +117,13 @@ export function getModemState() {
return apiGet("/modem/state"); return apiGet("/modem/state");
} }
export function setModemFrequency(newFrequency) { export function setRadioParameters(frequency, mode, rf_level) {
console.error("setModemFrequency needs implemented"); return apiPost("/radio", {
radio_frequency: frequency,
radio_mode: mode,
radio_rf_level: rf_level,
});
} }
export function setModemRigMode(mode) { export function getRadioStatus() {
console.error("setModemRigMode needs implemented"); return apiGet("/radio");
}
export function setModemRigPowerLvl(power) {
console.error("setModemRigPowerLvl needs implemented");
} }

View file

@ -1,5 +1,3 @@
import { addDataToWaterfall } from "../js/waterfallHandler.js";
import { import {
newMessageReceived, newMessageReceived,
newBeaconReceived, newBeaconReceived,
@ -23,8 +21,10 @@ export function connectionFailed(endpoint, event) {
} }
export function stateDispatcher(data) { export function stateDispatcher(data) {
data = JSON.parse(data); data = JSON.parse(data);
//console.log(data); //console.debug(data);
if (data["type"] == "state-change" || data["type"] == "state") { if (data["type"] == "state-change" || data["type"] == "state") {
stateStore.modem_connection = "connected";
stateStore.channel_busy = data["channel_busy"]; stateStore.channel_busy = data["channel_busy"];
stateStore.is_codec2_traffic = data["is_codec2_traffic"]; stateStore.is_codec2_traffic = data["is_codec2_traffic"];
stateStore.is_modem_running = data["is_modem_running"]; stateStore.is_modem_running = data["is_modem_running"];
@ -32,6 +32,12 @@ export function stateDispatcher(data) {
stateStore.dbfs_level_percent = Math.round( stateStore.dbfs_level_percent = Math.round(
Math.pow(10, data["audio_dbfs"] / 20) * 100, 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.channel_busy_slot = data["channel_busy_slot"];
stateStore.beacon_state = data["is_beacon_running"]; stateStore.beacon_state = data["is_beacon_running"];
stateStore.radio_status = data["radio_status"]; stateStore.radio_status = data["radio_status"];
@ -45,7 +51,7 @@ export function stateDispatcher(data) {
export function eventDispatcher(data) { export function eventDispatcher(data) {
data = JSON.parse(data); data = JSON.parse(data);
//console.info(data); console.debug(data);
if (data["scatter"] !== undefined) { if (data["scatter"] !== undefined) {
stateStore.scatter = JSON.parse(data["scatter"]); stateStore.scatter = JSON.parse(data["scatter"]);
@ -99,22 +105,31 @@ export function eventDispatcher(data) {
case "NEW": 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}`; 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); 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; return;
case "OPEN_SENT": case "OPEN_SENT":
console.log("state OPEN_SENT needs to be implemented"); console.info("state OPEN_SENT needs to be implemented");
return; return;
case "INFO_SENT": case "INFO_SENT":
console.log("state INFO_SENT needs to be implemented"); console.info("state INFO_SENT needs to be implemented");
return; return;
case "BURST_SENT": 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}`; 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); 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; return;
case "ABORTING": case "ABORTING":
console.log("state ABORTING needs to be implemented"); console.info("state ABORTING needs to be implemented");
return; return;
case "ABORTED": case "ABORTED":
@ -150,21 +165,42 @@ export function eventDispatcher(data) {
case "NEW": 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}`; 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); 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; return;
case "OPEN_ACK_SENT": 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}`; 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); 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; return;
case "INFO_ACK_SENT": 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}`; 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); 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; return;
case "BURST_REPLY_SENT": 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}`; 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); 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; return;
case "ENDED": case "ENDED":
@ -175,10 +211,16 @@ export function eventDispatcher(data) {
data["arq-transfer-inbound"].data, data["arq-transfer-inbound"].data,
data["arq-transfer-inbound"], 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; return;
case "ABORTED": case "ABORTED":
console.log("state ABORTED needs to be implemented"); console.info("state ABORTED needs to be implemented");
return; return;
case "FAILED": case "FAILED":

View file

@ -61,7 +61,7 @@ export function sortByPropertyDesc(property) {
* @returns true or false if callsign appears to be valid with an SSID * @returns true or false if callsign appears to be valid with an SSID
*/ */
export function validateCallsignWithSSID(callsign: string) { export function validateCallsignWithSSID(callsign: string) {
var patt = new RegExp("^[A-Z,a-z]+[0-9][A-Z,a-z]*-(1[0-5]|[0-9])$"); var patt = new RegExp("^[A-Z]+[0-9][A-Z]*-(1[0-5]|[0-9])$");
if ( if (
callsign === undefined || callsign === undefined ||
@ -82,7 +82,7 @@ export function validateCallsignWithSSID(callsign: string) {
* @returns true or false if callsign appears to be valid without an SSID * @returns true or false if callsign appears to be valid without an SSID
*/ */
export function validateCallsignWithoutSSID(callsign: string) { export function validateCallsignWithoutSSID(callsign: string) {
var patt = new RegExp("^[A-Za-z]+[0-9][A-Za-z]+$"); var patt = new RegExp("^[A-Z]+[0-9][A-Z]+$");
if ( if (
callsign === undefined || callsign === undefined ||

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

@ -17,11 +17,10 @@ nconf.defaults({
port: "5000", port: "5000",
spectrum: "waterfall", spectrum: "waterfall",
wf_theme: 2, wf_theme: 2,
theme: "default_light",
high_graphics: true,
update_channel: "alpha", update_channel: "alpha",
enable_sys_notification: false, enable_sys_notification: false,
grid_layout: "[]", grid_layout: "[]",
grid_preset: "[]",
}, },
}); });
@ -33,11 +32,10 @@ export const settingsStore = reactive({
port: "5000", port: "5000",
spectrum: "waterfall", spectrum: "waterfall",
wf_theme: 2, wf_theme: 2,
theme: "default_light",
high_graphics: true,
update_channel: "alpha", update_channel: "alpha",
enable_sys_notification: false, enable_sys_notification: false,
grid_layout: "[]", grid_layout: "[]",
grid_preset: "[]",
}, },
remote: { remote: {
AUDIO: { AUDIO: {

View file

@ -69,9 +69,6 @@ export const useStateStore = defineStore("stateStore", () => {
modem_connection.value = state; modem_connection.value = state;
if (modem_connection.value == "open") { if (modem_connection.value == "open") {
//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++;
} }

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([

View file

@ -4,6 +4,7 @@ import data_frame_factory
import structlog import structlog
from event_manager import EventManager from event_manager import EventManager
from modem_frametypes import FRAME_TYPE from modem_frametypes import FRAME_TYPE
import time
class ARQSession(): class ARQSession():
@ -44,6 +45,9 @@ class ARQSession():
self.event_frame_received = threading.Event() self.event_frame_received = threading.Event()
self.id = None self.id = None
self.session_started = time.time()
self.session_ended = 0
self.session_max_age = 500
def log(self, message, isWarning = False): def log(self, message, isWarning = False):
msg = f"[{type(self).__name__}][id={self.id}][state={self.state}]: {message}" msg = f"[{type(self).__name__}][id={self.id}][state={self.state}]: {message}"
@ -89,3 +93,29 @@ class ARQSession():
self.log(f"Ignoring unknow transition from state {self.state.name} with frame {frame['frame_type']}") 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
}

View file

@ -4,7 +4,7 @@ import helpers
from modem_frametypes import FRAME_TYPE from modem_frametypes import FRAME_TYPE
from codec2 import FREEDV_MODE from codec2 import FREEDV_MODE
from enum import Enum from enum import Enum
import time
class IRS_State(Enum): class IRS_State(Enum):
NEW = 0 NEW = 0
OPEN_ACK_SENT = 1 OPEN_ACK_SENT = 1
@ -66,6 +66,7 @@ class ARQSessionIRS(arq_session.ARQSession):
self.version = 1 self.version = 1
self.state = IRS_State.NEW self.state = IRS_State.NEW
self.state_enum = IRS_State # needed for access State enum from outside
self.total_length = 0 self.total_length = 0
self.total_crc = '' self.total_crc = ''
@ -93,12 +94,13 @@ class ARQSessionIRS(arq_session.ARQSession):
self.log(f"Waiting {timeout} seconds...") self.log(f"Waiting {timeout} seconds...")
if not self.event_frame_received.wait(timeout): if not self.event_frame_received.wait(timeout):
self.log("Timeout waiting for ISS. Session failed.") self.log("Timeout waiting for ISS. Session failed.")
self.session_ended = time.time()
self.set_state(IRS_State.FAILED) self.set_state(IRS_State.FAILED)
self.event_manager.send_arq_session_finished(False, self.id, self.dxcall, self.total_length, False, self.state.name) 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): def launch_transmit_and_wait(self, frame, timeout, mode):
thread_wait = threading.Thread(target = self.transmit_and_wait, thread_wait = threading.Thread(target = self.transmit_and_wait,
args = [frame, timeout, mode]) args = [frame, timeout, mode], daemon=True)
thread_wait.start() thread_wait.start()
def send_open_ack(self, open_frame): def send_open_ack(self, open_frame):
@ -185,9 +187,10 @@ class ARQSessionIRS(arq_session.ARQSession):
flag_checksum=True) flag_checksum=True)
self.transmit_frame(ack, mode=FREEDV_MODE.signalling) self.transmit_frame(ack, mode=FREEDV_MODE.signalling)
self.log("ACK sent") self.log("ACK sent")
self.session_ended = time.time()
self.set_state(IRS_State.ENDED) self.set_state(IRS_State.ENDED)
self.event_manager.send_arq_session_finished( self.event_manager.send_arq_session_finished(
False, self.id, self.dxcall, self.total_length, True, self.state.name, data=self.received_data) False, self.id, self.dxcall, True, self.state.name, data=self.received_data, statistics=self.calculate_session_statistics())
else: else:
@ -200,9 +203,10 @@ class ARQSessionIRS(arq_session.ARQSession):
flag_checksum=False) flag_checksum=False)
self.transmit_frame(ack, mode=FREEDV_MODE.signalling) self.transmit_frame(ack, mode=FREEDV_MODE.signalling)
self.log("CRC fail at the end of transmission!") self.log("CRC fail at the end of transmission!")
self.session_ended = time.time()
self.set_state(IRS_State.FAILED) self.set_state(IRS_State.FAILED)
self.event_manager.send_arq_session_finished( self.event_manager.send_arq_session_finished(
False, self.id, self.dxcall, self.total_length, False, self.state.name) False, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics())
def calibrate_speed_settings(self): def calibrate_speed_settings(self):
@ -226,4 +230,4 @@ class ARQSessionIRS(arq_session.ARQSession):
self.launch_transmit_and_wait(stop_ack, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling) self.launch_transmit_and_wait(stop_ack, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling)
self.set_state(IRS_State.ABORTED) self.set_state(IRS_State.ABORTED)
self.event_manager.send_arq_session_finished( self.event_manager.send_arq_session_finished(
False, self.id, self.dxcall, self.total_length, False, self.state.name) False, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics())

View file

@ -7,6 +7,7 @@ from modem_frametypes import FRAME_TYPE
import arq_session import arq_session
import helpers import helpers
from enum import Enum from enum import Enum
import time
class ISS_State(Enum): class ISS_State(Enum):
NEW = 0 NEW = 0
@ -56,11 +57,13 @@ class ARQSessionISS(arq_session.ARQSession):
super().__init__(config, modem, dxcall) super().__init__(config, modem, dxcall)
self.state_manager = state_manager self.state_manager = state_manager
self.data = data self.data = data
self.total_length = len(data)
self.data_crc = '' self.data_crc = ''
self.confirmed_bytes = 0 self.confirmed_bytes = 0
self.state = ISS_State.NEW self.state = ISS_State.NEW
self.state_enum = ISS_State # needed for access State enum from outside
self.id = self.generate_id() self.id = self.generate_id()
self.frame_factory = data_frame_factory.DataFrameFactory(self.config) self.frame_factory = data_frame_factory.DataFrameFactory(self.config)
@ -92,12 +95,12 @@ class ARQSessionISS(arq_session.ARQSession):
self.transmission_failed() self.transmission_failed()
def launch_twr(self, frame_or_burst, timeout, retries, mode): 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]) twr = threading.Thread(target = self.transmit_wait_and_retry, args=[frame_or_burst, timeout, retries, mode], daemon=True)
twr.start() twr.start()
def start(self): def start(self):
self.event_manager.send_arq_session_new( self.event_manager.send_arq_session_new(
True, self.id, self.dxcall, len(self.data), self.state.name) 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) 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.launch_twr(session_open_frame, self.TIMEOUT_CONNECT_ACK, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)
self.set_state(ISS_State.OPEN_SENT) self.set_state(ISS_State.OPEN_SENT)
@ -114,7 +117,7 @@ class ARQSessionISS(arq_session.ARQSession):
self.transmission_aborted(irs_frame) self.transmission_aborted(irs_frame)
return return
info_frame = self.frame_factory.build_arq_session_info(self.id, len(self.data), info_frame = self.frame_factory.build_arq_session_info(self.id, self.total_length,
helpers.get_crc_32(self.data), helpers.get_crc_32(self.data),
self.snr[0]) self.snr[0])
@ -127,9 +130,9 @@ class ARQSessionISS(arq_session.ARQSession):
if 'offset' in irs_frame: if 'offset' in irs_frame:
self.confirmed_bytes = irs_frame['offset'] self.confirmed_bytes = irs_frame['offset']
self.log(f"IRS confirmed {self.confirmed_bytes}/{len(self.data)} bytes") self.log(f"IRS confirmed {self.confirmed_bytes}/{self.total_length} bytes")
self.event_manager.send_arq_session_progress( self.event_manager.send_arq_session_progress(
True, self.id, self.dxcall, self.confirmed_bytes, len(self.data), self.state.name) True, self.id, self.dxcall, self.confirmed_bytes, self.total_length, self.state.name)
# check if we received an abort flag # check if we received an abort flag
if irs_frame["flag"]["ABORT"]: if irs_frame["flag"]["ABORT"]:
@ -137,7 +140,7 @@ class ARQSessionISS(arq_session.ARQSession):
return return
if irs_frame["flag"]["FINAL"]: if irs_frame["flag"]["FINAL"]:
if self.confirmed_bytes == len(self.data) and irs_frame["flag"]["CHECKSUM"]: if self.confirmed_bytes == self.total_length and irs_frame["flag"]["CHECKSUM"]:
self.transmission_ended(irs_frame) self.transmission_ended(irs_frame)
return return
else: else:
@ -158,15 +161,18 @@ class ARQSessionISS(arq_session.ARQSession):
def transmission_ended(self, irs_frame): def transmission_ended(self, irs_frame):
# final function for sucessfully ended transmissions # final function for sucessfully ended transmissions
self.session_ended = time.time()
self.set_state(ISS_State.ENDED) 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.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, len(self.data),True, self.state.name) 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): def transmission_failed(self, irs_frame=None):
# final function for failed transmissions # final function for failed transmissions
self.session_ended = time.time()
self.set_state(ISS_State.FAILED) self.set_state(ISS_State.FAILED)
self.log(f"Transmission failed!") self.log(f"Transmission failed!")
self.event_manager.send_arq_session_finished(True, self.id, self.dxcall, len(self.data),False, self.state.name) 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): def abort_transmission(self, irs_frame=None):
# function for starting the abort sequence # function for starting the abort sequence
@ -174,7 +180,7 @@ class ARQSessionISS(arq_session.ARQSession):
self.set_state(ISS_State.ABORTING) self.set_state(ISS_State.ABORTING)
self.event_manager.send_arq_session_finished( self.event_manager.send_arq_session_finished(
True, self.id, self.dxcall, len(self.data), False, self.state.name) True, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics())
# break actual retries # break actual retries
self.event_frame_received.set() self.event_frame_received.set()
@ -188,9 +194,12 @@ class ARQSessionISS(arq_session.ARQSession):
def transmission_aborted(self, irs_frame): def transmission_aborted(self, irs_frame):
self.log("session aborted") self.log("session aborted")
self.session_ended = time.time()
self.set_state(ISS_State.ABORTED) self.set_state(ISS_State.ABORTED)
# break actual retries # break actual retries
self.event_frame_received.set() self.event_frame_received.set()
self.event_manager.send_arq_session_finished( self.event_manager.send_arq_session_finished(
True, self.id, self.dxcall, len(self.data), False, self.state.name) 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

@ -1,47 +1,41 @@
import threading
import data_frame_factory
import command_beacon import command_beacon
import sched
import time
import threading
class Beacon: class Beacon:
BEACON_LOOP_INTERVAL = 1
def __init__(self, config, states, event_manager, logger, modem): def __init__(self, config, states, event_manager, logger, modem):
self.config = config
self.modem_config = config
self.states = states self.states = states
self.event_manager = event_manager self.event_manager = event_manager
self.log = logger self.log = logger
self.modem = modem self.modem = modem
self.loop_running = True self.scheduler = sched.scheduler(time.time, time.sleep)
self.paused = False self.beacon_interval = self.config['MODEM']['beacon_interval']
self.thread = None self.beacon_enabled = False
self.event = threading.Event() self.event = threading.Event()
self.frame_factory = data_frame_factory.DataFrameFactory(config)
def start(self): def start(self):
beacon_thread = threading.Thread(target=self.run_beacon, name="beacon", daemon=True) self.beacon_enabled = True
beacon_thread.start() self.schedule_beacon()
def stop(self): def stop(self):
self.loop_running = False 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): def refresh(self):
self.event.set() # Interrupt and reschedule the beacon
self.event.clear() self.scheduler = sched.scheduler(time.time, time.sleep)
self.schedule_beacon()
def run_beacon(self) -> None:
while self.loop_running:
while (self.states.is_beacon_running and
not self.paused and
True):
#not self.states.channel_busy):
cmd = command_beacon.BeaconCommand(self.modem_config, self.states, self.event_manager)
cmd.run(self.event_manager, self.modem)
self.event.wait(self.modem_config['MODEM']['beacon_interval'])
self.event.wait(self.BEACON_LOOP_INTERVAL)

View file

@ -101,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.info("[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

View file

@ -7,4 +7,4 @@ class TestCommand(TxCommand):
return self.frame_factory.build_test() return self.frame_factory.build_test()
def get_tx_mode(self): def get_tx_mode(self):
return codec2.FREEDV_MODE.datac13.value return codec2.FREEDV_MODE.datac3

View file

@ -138,9 +138,9 @@ class Demodulator():
def sd_input_audio_callback(self, indata: np.ndarray, frames: int, time, status) -> None: def sd_input_audio_callback(self, indata: np.ndarray, frames: int, time, status) -> None:
audio_48k = np.frombuffer(indata, dtype=np.int16) audio_48k = np.frombuffer(indata, dtype=np.int16)
audio_8k = self.resampler.resample48_to_8(audio_48k) audio_8k = self.resampler.resample48_to_8(audio_48k)
audio.calculate_fft(audio_8k, self.fft_queue, self.states)
audio_8k_level_adjusted = audio.set_audio_volume(audio_8k, self.rx_audio_level) audio_8k_level_adjusted = audio.set_audio_volume(audio_8k, self.rx_audio_level)
audio.calculate_fft(audio_8k_level_adjusted, self.fft_queue, self.states)
length_audio_8k_level_adjusted = len(audio_8k_level_adjusted) length_audio_8k_level_adjusted = len(audio_8k_level_adjusted)
# Avoid buffer overflow by filling only if buffer for # Avoid buffer overflow by filling only if buffer for

View file

@ -56,7 +56,9 @@ class EventManager:
} }
self.broadcast(event) self.broadcast(event)
def send_arq_session_finished(self, outbound: bool, session_id, dxcall, total_bytes, success: bool, state: bool, data=False): def send_arq_session_finished(self, outbound: bool, session_id, dxcall, success: bool, state: bool, data=False, statistics=None):
if statistics is None:
statistics = {}
if data: if data:
data = base64.b64encode(data).decode("UTF-8") data = base64.b64encode(data).decode("UTF-8")
direction = 'outbound' if outbound else 'inbound' direction = 'outbound' if outbound else 'inbound'
@ -65,7 +67,7 @@ class EventManager:
f"arq-transfer-{direction}": { f"arq-transfer-{direction}": {
'session_id': session_id, 'session_id': session_id,
'dxcall': dxcall, 'dxcall': dxcall,
'total_bytes': total_bytes, 'statistics': statistics,
'success': bool(success), 'success': bool(success),
'state': state, 'state': state,
'data': data 'data': data

View file

@ -11,6 +11,8 @@ import requests
import threading import threading
import ujson as json import ujson as json
import structlog import structlog
import sched
import time
log = structlog.get_logger("explorer") log = structlog.get_logger("explorer")
@ -21,18 +23,16 @@ class explorer():
self.states = states self.states = states
self.explorer_url = "https://api.freedata.app/explorer.php" self.explorer_url = "https://api.freedata.app/explorer.php"
self.publish_interval = 120 self.publish_interval = 120
self.enable_explorer = config["STATION"]["enable_explorer"]
if self.enable_explorer: self.scheduler = sched.scheduler(time.time, time.sleep)
self.interval_thread = threading.Thread(target=self.interval, name="interval", daemon=True) self.schedule_thread = threading.Thread(target=self.run_scheduler)
self.interval_thread.start() self.schedule_thread.start()
def interval(self): def run_scheduler(self):
# Wait for just a little bit incase modem is contionously restarting due to a bug or user configuration issue # Schedule the first execution of push
threading.Event().wait(30) self.scheduler.enter(self.publish_interval, 1, self.push)
while True: # Run the scheduler in a loop
self.push() self.scheduler.run()
threading.Event().wait(self.publish_interval)
def push(self): def push(self):
@ -43,7 +43,7 @@ class explorer():
version = str(self.app.MODEM_VERSION) version = str(self.app.MODEM_VERSION)
bandwidth = str(self.config['MODEM']['enable_low_bandwidth_mode']) bandwidth = str(self.config['MODEM']['enable_low_bandwidth_mode'])
beacon = str(self.states.is_beacon_running) beacon = str(self.states.is_beacon_running)
strength = str(self.states.radio_strength) strength = str(self.states.s_meter_strength)
log.info("[EXPLORER] publish", frequency=frequency, band=band, callsign=callsign, gridsquare=gridsquare, version=version, bandwidth=bandwidth) log.info("[EXPLORER] publish", frequency=frequency, band=band, callsign=callsign, gridsquare=gridsquare, version=version, bandwidth=bandwidth)
@ -67,8 +67,14 @@ class explorer():
station_data = json.dumps(station_data) station_data = json.dumps(station_data)
try: try:
response = requests.post(self.explorer_url, json=station_data, headers=headers) response = requests.post(self.explorer_url, json=station_data, headers=headers)
# print(response.status_code)
# print(response.content)
except Exception as e: except Exception as e:
log.warning("[EXPLORER] connection lost") log.warning("[EXPLORER] connection lost")
# Reschedule the push method
self.scheduler.enter(self.publish_interval, 1, self.push)
def shutdown(self):
# If there are other cleanup tasks, include them here
if self.schedule_thread:
self.schedule_thread.join()

View file

@ -97,41 +97,3 @@ class DISPATCHER():
if data[:1] == FR_TYPE.ARQ_SESSION_OPEN: if data[:1] == FR_TYPE.ARQ_SESSION_OPEN:
return data[13:14] return data[13:14]
return None return None

View file

@ -28,6 +28,42 @@ class FrameHandler():
'bytes_per_frame': 0 'bytes_per_frame': 0
} }
def is_frame_for_me(self):
call_with_ssid = self.config['STATION']['mycall'] + "-" + str(self.config['STATION']['myssid'])
ft = self.details['frame']['frame_type']
print(self.details)
# Check for callsign checksum
if ft in ['ARQ_SESSION_OPEN', 'ARQ_SESSION_OPEN_ACK', 'PING', 'PING_ACK']:
valid, mycallsign = helpers.check_callsign(
call_with_ssid,
self.details["frame"]["destination_crc"],
self.config['STATION']['ssid_list'])
# Check for session id on IRS side
elif ft in ['ARQ_SESSION_INFO', 'ARQ_BURST_FRAME', 'ARQ_STOP']:
session_id = self.details['frame']['session_id']
if session_id in self.states.arq_irs_sessions:
valid = True
# Check for session id on ISS side
elif ft in ['ARQ_SESSION_INFO_ACK', 'ARQ_BURST_ACK', 'ARQ_STOP_ACK']:
session_id = self.details['frame']['session_id']
if session_id in self.states.arq_iss_sessions:
valid = True
else:
valid = False
if not valid:
self.logger.info(f"[Frame handler] {ft} received but not for us.")
return valid
def should_respond(self):
return self.is_frame_for_me()
def add_to_activity_list(self): def add_to_activity_list(self):

View file

@ -9,6 +9,10 @@ from arq_session_iss import ARQSessionISS
class ARQFrameHandler(frame_handler.FrameHandler): class ARQFrameHandler(frame_handler.FrameHandler):
def follow_protocol(self): def follow_protocol(self):
if not self.should_respond():
return
frame = self.details['frame'] frame = self.details['frame']
session_id = frame['session_id'] session_id = frame['session_id']
snr = self.details["snr"] snr = self.details["snr"]
@ -22,6 +26,9 @@ class ARQFrameHandler(frame_handler.FrameHandler):
# Normal case when receiving a SESSION_OPEN for the first time # Normal case when receiving a SESSION_OPEN for the first time
else: else:
if self.states.check_if_running_arq_session():
self.logger.warning("DISCARDING SESSION OPEN because of ongoing ARQ session ", frame=frame)
return
session = ARQSessionIRS(self.config, session = ARQSessionIRS(self.config,
self.modem, self.modem,
frame['origin'], frame['origin'],

View file

@ -4,21 +4,20 @@ import data_frame_factory
class PingFrameHandler(frame_handler.FrameHandler): class PingFrameHandler(frame_handler.FrameHandler):
def is_frame_for_me(self): #def is_frame_for_me(self):
call_with_ssid = self.config['STATION']['mycall'] + "-" + str(self.config['STATION']['myssid']) # call_with_ssid = self.config['STATION']['mycall'] + "-" + str(self.config['STATION']['myssid'])
valid, mycallsign = helpers.check_callsign( # valid, mycallsign = helpers.check_callsign(
call_with_ssid, # call_with_ssid,
self.details["frame"]["destination_crc"], # self.details["frame"]["destination_crc"],
self.config['STATION']['ssid_list']) # self.config['STATION']['ssid_list'])
if not valid: # if not valid:
ft = self.details['frame']['frame_type'] # ft = self.details['frame']['frame_type']
self.logger.info(f"[Modem] {ft} received but not for us.") # self.logger.info(f"[Modem] {ft} received but not for us.")
# return valid
return valid #def should_respond(self):
# return self.is_frame_for_me()
def should_respond(self):
return self.is_frame_for_me()
def follow_protocol(self): def follow_protocol(self):

View file

@ -93,15 +93,9 @@ def get_crc_24(data: str) -> bytes:
""" """
if not isinstance(data, (bytes)) or isinstance(data, (bytearray)): if not isinstance(data, (bytes)) or isinstance(data, (bytearray)):
data = bytes(data,'utf-8') data = bytes(data,'utf-8')
crc_algorithm = crcengine.create(
0x864CFB, params = crcengine.CrcParams(0x864cfb, 24, 0xb704ce, reflect_in=False, reflect_out=False, xor_out=0)
24, crc_algorithm = crcengine.create(params=params)
0xB704CE,
ref_in=False,
ref_out=False,
xor_out=0,
name="crc-24-openpgp",
)
return crc_algorithm(data).to_bytes(3,byteorder="big") return crc_algorithm(data).to_bytes(3,byteorder="big")
@ -302,25 +296,28 @@ def check_callsign(callsign: str, crc_to_check: bytes, ssid_list):
[True, Callsign + SSID] [True, Callsign + SSID]
False False
""" """
print(callsign)
if not isinstance(callsign, (bytes)): if not isinstance(callsign, (bytes)):
callsign = bytes(callsign,'utf-8') callsign = bytes(callsign,'utf-8')
log.debug("[HLP] check_callsign: Checking:", callsign=callsign)
try: try:
# We want the callsign without SSID # We want the callsign without SSID
callsign = callsign.split(b"-")[0] splitted_callsign = callsign.split(b"-")
callsign = splitted_callsign[0]
ssid = splitted_callsign[1].decode()
except IndexError: except IndexError:
# This is expected when `callsign` doesn't have a dash. # This is expected when `callsign` doesn't have a dash.
pass ssid = 0
except Exception as err: except Exception as err:
log.debug("[HLP] check_callsign: Error converting to bytes:", e=err) log.debug("[HLP] check_callsign: Error converting to bytes:", e=err)
# ensure, we are always have the own ssid in ssid_list even if it is empty
if ssid not in ssid_list:
ssid_list.append(str(ssid))
for ssid in ssid_list: for ssid in ssid_list:
call_with_ssid = callsign + b'-' + (str(ssid)).encode('utf-8') call_with_ssid = callsign + b'-' + (str(ssid)).encode('utf-8')
#call_with_ssid.extend("-".encode("utf-8"))
#call_with_ssid.extend(str(ssid).encode("utf-8"))
callsign_crc = get_crc_24(call_with_ssid) callsign_crc = get_crc_24(call_with_ssid)
callsign_crc = callsign_crc.hex() callsign_crc = callsign_crc.hex()
@ -328,6 +325,7 @@ def check_callsign(callsign: str, crc_to_check: bytes, ssid_list):
log.debug("[HLP] check_callsign matched:", call_with_ssid=call_with_ssid, checksum=crc_to_check) log.debug("[HLP] check_callsign matched:", call_with_ssid=call_with_ssid, checksum=crc_to_check)
return [True, call_with_ssid.decode()] return [True, call_with_ssid.decode()]
log.debug("[HLP] check_callsign: Checking:", callsign=callsign, crc_to_check=crc_to_check, own_crc=callsign_crc)
return [False, b''] return [False, b'']

View file

@ -31,11 +31,12 @@ class RF:
log = structlog.get_logger("RF") log = structlog.get_logger("RF")
def __init__(self, config, event_manager, fft_queue, service_queue, states) -> None: def __init__(self, config, event_manager, fft_queue, service_queue, states, radio_manager) -> None:
self.config = config self.config = config
self.service_queue = service_queue self.service_queue = service_queue
self.states = states self.states = states
self.event_manager = event_manager self.event_manager = event_manager
self.radio = radio_manager
self.sampler_avg = 0 self.sampler_avg = 0
self.buffer_avg = 0 self.buffer_avg = 0
@ -108,7 +109,6 @@ class RF:
# Initialize codec2, rig control, and data threads # Initialize codec2, rig control, and data threads
self.init_codec2() self.init_codec2()
self.init_rig_control()
self.init_data_threads() self.init_data_threads()
return True return True
@ -423,73 +423,3 @@ class RF:
else: else:
sd.play(audio_48k, blocksize=4096, blocking=True) sd.play(audio_48k, blocksize=4096, blocking=True)
return return
def init_rig_control(self):
# Check how we want to control the radio
if self.radiocontrol == "rigctld":
import rigctld as rig
elif self.radiocontrol == "tci":
self.radio = self.tci_module
else:
import rigdummy as rig
if not self.radiocontrol in ["tci"]:
self.radio = rig.radio()
self.radio.open_rig(
rigctld_ip=self.rigctld_ip,
rigctld_port=self.rigctld_port,
)
hamlib_thread = threading.Thread(
target=self.update_rig_data, name="HAMLIB_THREAD", daemon=True
)
hamlib_thread.start()
hamlib_set_thread = threading.Thread(
target=self.set_rig_data, name="HAMLIB_SET_THREAD", daemon=True
)
hamlib_set_thread.start()
def set_rig_data(self) -> None:
"""
Set rigctld parameters like frequency, mode
THis needs to be processed in a queue
"""
while True:
cmd = RIGCTLD_COMMAND_QUEUE.get()
if cmd[0] == "set_frequency":
# [1] = Frequency
self.radio.set_frequency(cmd[1])
if cmd[0] == "set_mode":
# [1] = Mode
self.radio.set_mode(cmd[1])
def update_rig_data(self) -> None:
"""
Request information about the current state of the radio via hamlib
"""
while True:
try:
# this looks weird, but is necessary for avoiding rigctld packet colission sock
#threading.Event().wait(0.1)
self.states.set("radio_status", self.radio.get_status())
#threading.Event().wait(0.25)
self.states.set("radio_frequency", self.radio.get_frequency())
threading.Event().wait(0.1)
self.states.set("radio_mode", self.radio.get_mode())
threading.Event().wait(0.1)
self.states.set("radio_bandwidth", self.radio.get_bandwidth())
threading.Event().wait(0.1)
if self.states.isTransmitting():
self.radio_alc = self.radio.get_alc()
threading.Event().wait(0.1)
self.states.set("radio_rf_power", self.radio.get_level())
threading.Event().wait(0.1)
self.states.set("radio_strength", self.radio.get_strength())
except Exception as e:
self.log.warning(
"[MDM] error getting radio data",
e=e,
)
threading.Event().wait(1)

59
modem/radio_manager.py Normal file
View file

@ -0,0 +1,59 @@
import rigctld
import tci
import rigdummy
import time
import threading
class RadioManager:
def __init__(self, config, state_manager, event_manager):
self.config = config
self.state_manager = state_manager
self.event_manager = event_manager
self.radiocontrol = config['RADIO']['control']
self.rigctld_ip = config['RIGCTLD']['ip']
self.rigctld_port = config['RIGCTLD']['port']
self.refresh_rate = 1
self.stop_event = threading.Event()
self.update_thread = threading.Thread(target=self.update_parameters, daemon=True)
self._init_rig_control()
def _init_rig_control(self):
# Check how we want to control the radio
if self.radiocontrol == "rigctld":
self.radio = rigctld.radio(self.state_manager, hostname=self.rigctld_ip,port=self.rigctld_port)
elif self.radiocontrol == "tci":
raise NotImplementedError
# self.radio = self.tci_module
else:
self.radio = rigdummy.radio()
self.update_thread.start()
def set_ptt(self, state):
self.radio.set_ptt(state)
def set_frequency(self, frequency):
self.radio.set_frequency(frequency)
def set_mode(self, mode):
self.radio.set_mode(mode)
def set_rf_level(self, level):
self.radio.set_rf_level(level)
def update_parameters(self):
while not self.stop_event.is_set():
parameters = self.radio.get_parameters()
self.state_manager.set("radio_frequency", parameters['frequency'])
self.state_manager.set("radio_mode", parameters['mode'])
self.state_manager.set("radio_bandwidth", parameters['bandwidth'])
self.state_manager.set("radio_rf_level", parameters['rf'])
if self.state_manager.isTransmitting():
self.radio_alc = parameters['alc']
self.state_manager.set("s_meter_strength", parameters['strength'])
time.sleep(self.refresh_rate)
def stop(self):
self.radio.disconnect()
self.stop_event.set()

View file

@ -1,12 +1,6 @@
#!/usr/bin/env python3
# class taken from darksidelemm
# rigctl - https://github.com/darksidelemm/rotctld-web-gui/blob/master/rotatorgui.py#L35
#
# modified and adjusted to FreeDATA needs by DJ2LS
import contextlib
import socket import socket
import structlog import structlog
import time
import threading import threading
class radio: class radio:
@ -14,332 +8,198 @@ class radio:
log = structlog.get_logger("radio (rigctld)") log = structlog.get_logger("radio (rigctld)")
def __init__(self, hostname="localhost", port=4532, poll_rate=5, timeout=5): def __init__(self, states, hostname="localhost", port=4532, timeout=5):
"""Open a connection to rigctld, and test it for validity"""
self.ptt_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.data_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.ptt_connected = False
self.data_connected = False
self.hostname = hostname self.hostname = hostname
self.port = port self.port = port
self.connection_attempts = 5 self.timeout = timeout
self.states = states
# class wide variable for some parameters self.connection = None
self.bandwidth = '' self.connected = False
self.frequency = '' self.await_response = threading.Event()
self.mode = '' self.await_response.set()
self.alc = ''
self.strength = ''
self.rf = ''
def open_rig( self.parameters = {
self, 'frequency': '---',
rigctld_ip, 'mode': '---',
rigctld_port 'alc': '---',
): 'strength': '---',
""" 'bandwidth': '---',
'rf': '---',
'ptt': False # Initial PTT state is set to False
}
Args: # connect to radio
rigctld_ip: self.connect()
rigctld_port:
Returns: def connect(self):
"""
self.hostname = rigctld_ip
self.port = int(rigctld_port)
# _ptt_connect = self.ptt_connect()
# _data_connect = self.data_connect()
ptt_thread = threading.Thread(target=self.ptt_connect, args=[], daemon=True)
ptt_thread.start()
data_thread = threading.Thread(target=self.data_connect, args=[], daemon=True)
data_thread.start()
# wait some time
threading.Event().wait(0.5)
if self.ptt_connected and self.data_connected:
self.log.debug("Rigctl DATA/PTT initialized")
return True
self.log.error(
"[RIGCTLD] Can't connect!", ip=self.hostname, port=self.port
)
return False
def ptt_connect(self):
"""Connect to rigctld instance"""
while True:
if not self.ptt_connected:
try: try:
self.ptt_connection = socket.create_connection((self.hostname, self.port)) self.connection = socket.create_connection((self.hostname, self.port), timeout=self.timeout)
self.ptt_connected = True self.connected = True
self.log.info( self.states.set("radio_status", True)
"[RIGCTLD] Connected PTT instance to rigctld!", ip=self.hostname, port=self.port self.log.info(f"[RIGCTLD] Connected to rigctld at {self.hostname}:{self.port}")
)
except Exception as err: except Exception as err:
# ConnectionRefusedError: [Errno 111] Connection refused self.log.warning(f"[RIGCTLD] Failed to connect to rigctld: {err}")
self.close_rig() self.connected = False
self.log.warning( self.states.set("radio_status", False)
"[RIGCTLD] PTT Reconnect...",
ip=self.hostname,
port=self.port,
e=err,
)
threading.Event().wait(0.5) def disconnect(self):
self.connected = False
self.connection.close()
del self.connection
self.connection = None
self.states.set("radio_status", False)
self.parameters = {
'frequency': '---',
'mode': '---',
'alc': '---',
'strength': '---',
'bandwidth': '---',
'rf': '---',
'ptt': False # Initial PTT state is set to False
}
def send_command(self, command) -> str:
if self.connected:
# wait if we have another command awaiting its response...
self.await_response.wait()
def data_connect(self):
"""Connect to rigctld instance"""
while True:
if not self.data_connected:
try: try:
self.data_connection = socket.create_connection((self.hostname, self.port)) self.await_response = threading.Event()
self.data_connected = True self.connection.sendall(command.encode('utf-8') + b"\n")
self.log.info( response = self.connection.recv(1024)
"[RIGCTLD] Connected DATA instance to rigctld!", ip=self.hostname, port=self.port self.await_response.set()
) return response.decode('utf-8').strip()
except Exception as err: except Exception as err:
# ConnectionRefusedError: [Errno 111] Connection refused self.log.warning(f"[RIGCTLD] Error sending command [{command}] to rigctld: {err}")
self.close_rig() self.connected = False
self.log.warning(
"[RIGCTLD] DATA Reconnect...",
ip=self.hostname,
port=self.port,
e=err,
)
threading.Event().wait(0.5)
def close_rig(self):
""" """
self.ptt_sock.close()
self.data_sock.close()
self.ptt_connected = False
self.data_connected = False
def send_ptt_command(self, command, expect_answer) -> bytes:
"""Send a command to the connected rotctld instance,
and return the return value.
Args:
command:
"""
if self.ptt_connected:
try:
self.ptt_connection.sendall(command + b"\n")
except Exception:
self.log.warning(
"[RIGCTLD] Command not executed!",
command=command,
ip=self.hostname,
port=self.port,
)
self.ptt_connected = False
return b""
def send_data_command(self, command, expect_answer) -> bytes:
"""Send a command to the connected rotctld instance,
and return the return value.
Args:
command:
"""
if self.data_connected:
self.data_connection.setblocking(False)
#Allow a little more time for a response from rigctld before generating a timeout, seems to have no ill effects on a well behaving setup and fixes Issue #373
self.data_connection.settimeout(0.30)
try:
self.data_connection.sendall(command + b"\n")
except Exception: return ""
self.log.warning(
"[RIGCTLD] Command not executed!",
command=command,
ip=self.hostname,
port=self.port,
)
self.data_connected = False
try:
# recv seems to be blocking so in case of ptt we don't need the response
# maybe this speeds things up and avoids blocking states
recv = True
data = b''
while recv:
try:
data = self.data_connection.recv(4800)
except socket.timeout:
recv = False
return data
# return self.data_connection.recv(64) if expect_answer else True
except Exception:
self.log.warning(
"[RIGCTLD] No command response!",
command=command,
ip=self.hostname,
port=self.port,
)
self.data_connected = False
return b""
def get_status(self):
""" """
return True if self.data_connected and self.ptt_connected else False
def get_level(self):
try:
data = self.send_data_command(b"l RF", True)
data = data.split(b"\n")
rf = data[0].decode("utf-8")
if 'RPRT' not in rf:
try:
self.rf = str(rf)
except ValueError:
self.rf = str(rf)
return self.rf
except Exception:
return self.rf
def get_strength(self):
try:
data = self.send_data_command(b"l STRENGTH", True)
data = data.split(b"\n")
strength = data[0].decode("utf-8")
if 'RPRT' not in strength:
try:
self.strength = str(strength)
except ValueError:
self.strength = str(strength)
return self.strength
except Exception:
return self.strength
def get_alc(self):
try:
data = self.send_data_command(b"l ALC", True)
data = data.split(b"\n")
alc = data[0].decode("utf-8")
if 'RPRT' not in alc:
try:
alc = float(alc)
except ValueError:
self.alc = 0.0
return self.alc
except Exception:
return self.alc
def get_mode(self):
""" """
try:
data = self.send_data_command(b"m", True)
data = data.split(b"\n")
data = data[0].decode("utf-8")
if 'RPRT' not in data:
try:
data = int(data)
except ValueError:
self.mode = str(data)
return self.mode
except Exception:
return self.mode
def get_bandwidth(self):
""" """
try:
data = self.send_data_command(b"m", True)
data = data.split(b"\n")
data = data[1].decode("utf-8")
if 'RPRT' not in data and data not in ['']:
with contextlib.suppress(ValueError):
self.bandwidth = int(data)
return self.bandwidth
except Exception:
return self.bandwidth
def get_frequency(self):
""" """
try:
data = self.send_data_command(b"f", True)
data = data.decode("utf-8")
if 'RPRT' not in data and data not in [0, '0', '']:
with contextlib.suppress(ValueError):
data = int(data)
# make sure we have a frequency and not bandwidth
if data >= 10000:
self.frequency = data
return self.frequency
except Exception:
return self.frequency
def get_ptt(self):
""" """
try:
return self.send_data_command(b"t", True)
except Exception:
return False
def set_ptt(self, state): def set_ptt(self, state):
""" """Set the PTT (Push-to-Talk) state.
Args: Args:
state: state (bool): True to enable PTT, False to disable.
Returns: Returns:
bool: True if the PTT state was set successfully, False otherwise.
""" """
if self.connected:
try: try:
if state: if state:
self.send_ptt_command(b"T 1", False) self.send_command('T 1') # Enable PTT
else: else:
self.send_ptt_command(b"T 0", False) self.send_command('T 0') # Disable PTT
return state self.parameters['ptt'] = state # Update PTT state in parameters
except Exception: return True
return False except Exception as err:
self.log.warning(f"[RIGCTLD] Error setting PTT state: {err}")
def set_frequency(self, frequency): self.connected = False
"""
Args:
frequency:
Returns:
"""
try:
command = bytes(f"F {frequency}", "utf-8")
self.send_data_command(command, False)
except Exception:
return False return False
def set_mode(self, mode): def set_mode(self, mode):
""" """Set the mode.
Args: Args:
mode: mode (str): The mode to set.
Returns: Returns:
bool: True if the mode was set successfully, False otherwise.
""" """
if self.connected:
try: try:
command = bytes(f"M {mode} {self.bandwidth}", "utf-8") command = f"M {mode} 0"
self.send_data_command(command, False) self.send_command(command)
except Exception: self.parameters['mode'] = mode
return True
except Exception as err:
self.log.warning(f"[RIGCTLD] Error setting mode: {err}")
self.connected = False
return False return False
def set_frequency(self, frequency):
"""Set the frequency.
Args:
frequency (str): The frequency to set.
Returns:
bool: True if the frequency was set successfully, False otherwise.
"""
if self.connected:
try:
command = f"F {frequency}"
self.send_command(command)
self.parameters['frequency'] = frequency
return True
except Exception as err:
self.log.warning(f"[RIGCTLD] Error setting frequency: {err}")
self.connected = False
return False
def set_bandwidth(self, bandwidth):
"""Set the bandwidth.
Args:
bandwidth (str): The bandwidth to set.
Returns:
bool: True if the bandwidth was set successfully, False otherwise.
"""
if self.connected:
try:
command = f"M {self.parameters['mode']} {bandwidth}"
self.send_command(command)
self.parameters['bandwidth'] = bandwidth
return True
except Exception as err:
self.log.warning(f"[RIGCTLD] Error setting bandwidth: {err}")
self.connected = False
return False
def set_rf_level(self, rf):
"""Set the RF.
Args:
rf (str): The RF to set.
Returns:
bool: True if the RF was set successfully, False otherwise.
"""
if self.connected:
try:
command = f"L RFPOWER {rf/100}" #RF RFPOWER --> RFPOWER == IC705
self.send_command(command)
self.parameters['rf'] = rf
return True
except Exception as err:
self.log.warning(f"[RIGCTLD] Error setting RF: {err}")
self.connected = False
return False
def get_parameters(self):
if not self.connected:
self.connect()
if self.connected:
self.parameters['frequency'] = self.send_command('f')
response = self.send_command(
'm').strip() # Get the mode/bandwidth response and remove leading/trailing spaces
try:
mode, bandwidth = response.split('\n', 1) # Split the response into mode and bandwidth
except ValueError:
print(response)
mode = 'err'
bandwidth = 'err'
self.parameters['mode'] = mode
self.parameters['bandwidth'] = bandwidth
self.parameters['alc'] = self.send_command('l ALC')
self.parameters['strength'] = self.send_command('l STRENGTH')
self.parameters['rf'] = self.send_command('l RFPOWER') # RF, RFPOWER
"""Return the latest fetched parameters."""
return self.parameters

View file

@ -1,13 +1,30 @@
hamlib_version = 0
class radio: class radio:
""" """ """ """
def __init__(self): def __init__(self):
pass self.parameters = {
'frequency': '---',
'mode': '---',
'alc': '---',
'strength': '---',
'bandwidth': '---',
'rf': '---',
'ptt': False # Initial PTT state is set to False
}
def open_rig(self, **kwargs): def connect(self, **kwargs):
"""
Args:
**kwargs:
Returns:
"""
return True
def disconnect(self, **kwargs):
""" """
Args: Args:
@ -98,3 +115,7 @@ class radio:
def close_rig(self): def close_rig(self):
""" """ """ """
return return
def get_parameters(self):
return self.parameters

View file

@ -11,7 +11,6 @@ import state_manager
import ujson as json import ujson as json
import websocket_manager as wsm import websocket_manager as wsm
import api_validations as validations import api_validations as validations
import command_ping
import command_cq import command_cq
import command_ping import command_ping
import command_feq import command_feq
@ -23,10 +22,7 @@ app = Flask(__name__)
CORS(app) CORS(app)
CORS(app, resources={r"/*": {"origins": "*"}}) CORS(app, resources={r"/*": {"origins": "*"}})
sock = Sock(app) sock = Sock(app)
app.config['SOCK_SERVER_OPTIONS'] = {'ping_interval': 10} MODEM_VERSION = "0.12.0-alpha"
# define global MODEM_VERSION
app.MODEM_VERSION = "0.12.0-alpha"
# set config file to use # set config file to use
def set_config(): def set_config():
@ -41,25 +37,10 @@ def set_config():
print(f"Config file '{config_file}' not found. Exiting.") print(f"Config file '{config_file}' not found. Exiting.")
exit(1) exit(1)
app.config_manager = CONFIG(config_file) return config_file
set_config()
# start modem
app.state_queue = queue.Queue() # queue which holds latest states
app.modem_events = queue.Queue() # queue which holds latest events
app.modem_fft = queue.Queue() # queue which holds latest fft data
app.modem_service = queue.Queue() # start / stop modem service
app.event_manager = event_manager.EventManager([app.modem_events]) # TODO remove the app.modem_event custom queue
# init state manager
app.state_manager = state_manager.StateManager(app.state_queue)
# start service manager
app.service_manager = service_manager.SM(app)
# start modem service
app.modem_service.put("start")
# returns a standard API response # returns a standard API response
def api_response(data, status = 200): def api_response(data, status = 200):
@ -95,7 +76,7 @@ def index():
return api_response({'name': 'FreeDATA API', return api_response({'name': 'FreeDATA API',
'description': '', 'description': '',
'api_version': 1, 'api_version': 1,
'modem_version': app.MODEM_VERSION, 'modem_version': MODEM_VERSION,
'license': 'GPL3.0', 'license': 'GPL3.0',
'documentation': 'https://wiki.freedata.app', 'documentation': 'https://wiki.freedata.app',
}) })
@ -221,6 +202,8 @@ def post_modem_send_raw():
return api_response({"info": "endpoint for SENDING RAW DATA via POST"}) return api_response({"info": "endpoint for SENDING RAW DATA via POST"})
if not app.state_manager.is_modem_running: if not app.state_manager.is_modem_running:
api_abort('Modem not running', 503) api_abort('Modem not running', 503)
if app.state_manager.check_if_running_arq_session():
api_abort('Modem busy', 503)
if enqueue_tx_command(command_arq_raw.ARQRawCommand, request.json): if enqueue_tx_command(command_arq_raw.ARQRawCommand, request.json):
return api_response(request.json) return api_response(request.json)
else: else:
@ -240,15 +223,21 @@ def post_modem_send_raw_stop():
return api_response(request.json) return api_response(request.json)
@app.route('/radio', methods=['GET', 'POST'])
def get_post_radio():
if request.method in ['POST']:
app.radio_manager.set_frequency(request.json['radio_frequency'])
app.radio_manager.set_mode(request.json['radio_mode'])
app.radio_manager.set_rf_level(int(request.json['radio_rf_level']))
return api_response(request.json)
elif request.method == 'GET':
return api_response(app.state_manager.get_radio_status())
# @app.route('/modem/arq_connect', methods=['POST']) # @app.route('/modem/arq_connect', methods=['POST'])
# @app.route('/modem/arq_disconnect', methods=['POST']) # @app.route('/modem/arq_disconnect', methods=['POST'])
# @app.route('/modem/send_raw', methods=['POST']) # @app.route('/modem/send_raw', methods=['POST'])
# @app.route('/modem/stop_transmission', methods=['POST'])
# @app.route('/modem/listen', methods=['POST']) # not needed if we are restarting modem on changing settings
# @app.route('/modem/record_audio', methods=['POST']) # @app.route('/modem/record_audio', methods=['POST'])
# @app.route('/modem/responde_to_call', methods=['POST']) # not needed if we are restarting modem on changing settings
# @app.route('/modem/responde_to_cq', methods=['POST']) # not needed if we are restarting modem on changing settings
# @app.route('/modem/audio_levels', methods=['POST']) # tx and rx # not needed if we are restarting modem on changing settings # @app.route('/modem/audio_levels', methods=['POST']) # tx and rx # not needed if we are restarting modem on changing settings
# @app.route('/modem/mesh_ping', methods=['POST']) # @app.route('/modem/mesh_ping', methods=['POST'])
# @app.route('/mesh/routing_table', methods=['GET']) # @app.route('/mesh/routing_table', methods=['GET'])
@ -272,4 +261,28 @@ def sock_fft(sock):
def sock_states(sock): def sock_states(sock):
wsm.handle_connection(sock, wsm.states_client_list, app.state_queue) wsm.handle_connection(sock, wsm.states_client_list, app.state_queue)
if __name__ == "__main__":
app.config['SOCK_SERVER_OPTIONS'] = {'ping_interval': 10}
# define global MODEM_VERSION
app.MODEM_VERSION = MODEM_VERSION
config_file = set_config()
app.config_manager = CONFIG(config_file)
# start modem
app.state_queue = queue.Queue() # queue which holds latest states
app.modem_events = queue.Queue() # queue which holds latest events
app.modem_fft = queue.Queue() # queue which holds latest fft data
app.modem_service = queue.Queue() # start / stop modem service
app.event_manager = event_manager.EventManager([app.modem_events]) # TODO remove the app.modem_event custom queue
# init state manager
app.state_manager = state_manager.StateManager(app.state_queue)
# start service manager
app.service_manager = service_manager.SM(app)
# start modem service
app.modem_service.put("start")
wsm.startThreads(app) wsm.startThreads(app)
app.run()

View file

@ -6,19 +6,21 @@ import audio
import ujson as json import ujson as json
import explorer import explorer
import beacon import beacon
import radio_manager
class SM: class SM:
def __init__(self, app): def __init__(self, app):
self.log = structlog.get_logger("service") self.log = structlog.get_logger("service")
self.app = app
self.modem = False self.modem = False
self.beacon = False self.beacon = False
self.app = app self.explorer = False
self.app.radio_manager = False
self.config = self.app.config_manager.read() self.config = self.app.config_manager.read()
self.modem_fft = app.modem_fft self.modem_fft = app.modem_fft
self.modem_service = app.modem_service self.modem_service = app.modem_service
self.states = app.state_manager self.state_manager = app.state_manager
self.event_manager = app.event_manager self.event_manager = app.event_manager
@ -27,27 +29,37 @@ class SM:
) )
runner_thread.start() runner_thread.start()
self.start_explorer_publishing()
def runner(self): def runner(self):
while True: while True:
cmd = self.modem_service.get() cmd = self.modem_service.get()
if cmd in ['start'] and not self.modem: if cmd in ['start'] and not self.modem:
self.log.info("------------------ FreeDATA ------------------") self.log.info("------------------ FreeDATA ------------------")
self.log.info("------------------ MODEM ------------------") self.log.info("------------------ MODEM ------------------")
self.config = self.app.config_manager.read()
self.start_radio_manager()
self.start_modem() self.start_modem()
self.start_explorer_publishing()
elif cmd in ['stop'] and self.modem: elif cmd in ['stop'] and self.modem:
self.stop_modem() self.stop_modem()
self.stop_explorer_publishing()
self.stop_radio_manager()
# we need to wait a bit for avoiding a portaudio crash # we need to wait a bit for avoiding a portaudio crash
threading.Event().wait(0.5) threading.Event().wait(0.5)
elif cmd in ['restart']: elif cmd in ['restart']:
self.stop_modem() self.stop_modem()
self.stop_explorer_publishing()
self.stop_radio_manager()
# we need to wait a bit for avoiding a portaudio crash # we need to wait a bit for avoiding a portaudio crash
threading.Event().wait(0.5) threading.Event().wait(0.5)
self.config = self.app.config_manager.read()
self.start_radio_manager()
if self.start_modem(): if self.start_modem():
self.event_manager.modem_restarted() self.event_manager.modem_restarted()
self.start_explorer_publishing()
elif cmd in ['start_beacon']: elif cmd in ['start_beacon']:
self.start_beacon() self.start_beacon()
@ -57,37 +69,34 @@ class SM:
else: else:
self.log.warning("[SVC] modem command processing failed", cmd=cmd, state=self.states.is_modem_running) self.log.warning("[SVC] modem command processing failed", cmd=cmd, state=self.state_manager.is_modem_running)
def start_modem(self): def start_modem(self):
# read config if self.state_manager.is_modem_running:
self.config = self.app.config_manager.read()
if self.states.is_modem_running:
self.log.warning("modem already running") self.log.warning("modem already running")
return False return False
# test audio devices # test audio devices
audio_test = self.test_audio() audio_test = self.test_audio()
if False in audio_test or None in audio_test or self.states.is_modem_running: if False in audio_test or None in audio_test or self.state_manager.is_modem_running:
self.log.warning("starting modem failed", input_test=audio_test[0], output_test=audio_test[1]) self.log.warning("starting modem failed", input_test=audio_test[0], output_test=audio_test[1])
self.states.set("is_modem_running", False) self.state_manager.set("is_modem_running", False)
self.event_manager.modem_failed() self.event_manager.modem_failed()
return False return False
self.log.info("starting modem....") self.log.info("starting modem....")
self.modem = modem.RF(self.config, self.event_manager, self.modem_fft, self.modem_service, self.states) self.modem = modem.RF(self.config, self.event_manager, self.modem_fft, self.modem_service, self.state_manager, self.app.radio_manager)
self.frame_dispatcher = frame_dispatcher.DISPATCHER(self.config, self.frame_dispatcher = frame_dispatcher.DISPATCHER(self.config,
self.event_manager, self.event_manager,
self.states, self.state_manager,
self.modem) self.modem)
self.frame_dispatcher.start() self.frame_dispatcher.start()
self.event_manager.modem_started() self.event_manager.modem_started()
self.states.set("is_modem_running", True) self.state_manager.set("is_modem_running", True)
self.modem.start_modem() self.modem.start_modem()
return True return True
@ -96,7 +105,7 @@ class SM:
self.log.info("stopping modem....") self.log.info("stopping modem....")
del self.modem del self.modem
self.modem = False self.modem = False
self.states.set("is_modem_running", False) self.state_manager.set("is_modem_running", False)
self.event_manager.modem_stopped() self.event_manager.modem_stopped()
def test_audio(self): def test_audio(self):
@ -111,16 +120,30 @@ class SM:
return [False, False] return [False, False]
def start_beacon(self): def start_beacon(self):
self.beacon = beacon.Beacon(self.config, self.states, self.event_manager, self.log, self.modem) self.beacon = beacon.Beacon(self.config, self.state_manager, self.event_manager, self.log, self.modem)
self.beacon.start() self.beacon.start()
def stop_beacon(self): def stop_beacon(self):
del self.beacon self.beacon.stop()
def start_explorer_publishing(self): def start_explorer_publishing(self):
try: try:
# optionally start explorer module # optionally start explorer module
if self.config['STATION']['enable_explorer']: if self.config['STATION']['enable_explorer']:
explorer.explorer(self.app, self.config, self.states) self.explorer = explorer.explorer(self.app, self.config, self.state_manager)
except Exception as e: except Exception as e:
self.log.warning("[EXPLORER] Publishin not started because of error", e=e) self.log.warning("[EXPLORER] Publishing not started because of error", e=e)
def stop_explorer_publishing(self):
if self.config['STATION']['enable_explorer']:
try:
del self.explorer
except Exception as e:
self.log.info("[EXPLORER] Error while stopping...", e=e)
def start_radio_manager(self):
self.app.radio_manager = radio_manager.RadioManager(self.config, self.state_manager, self.event_manager)
def stop_radio_manager(self):
self.app.radio_manager.stop()
del self.app.radio_manager

View file

@ -21,8 +21,6 @@ class StateManager:
self.is_modem_running = False self.is_modem_running = False
self.is_modem_busy = False self.is_modem_busy = False
self.is_beacon_running = False self.is_beacon_running = False
self.is_arq_state = False
self.is_arq_session = False
# If true, any wait() call is blocking # If true, any wait() call is blocking
self.transmitting_event = threading.Event() self.transmitting_event = threading.Event()
@ -43,8 +41,8 @@ class StateManager:
self.radio_frequency = 0 self.radio_frequency = 0
self.radio_mode = None self.radio_mode = None
self.radio_bandwidth = 0 self.radio_bandwidth = 0
self.radio_rf_power = 0 self.radio_rf_level = 0
self.radio_strength = 0 self.s_meter_strength = 0
# Set rig control status regardless or rig control method # Set rig control status regardless or rig control method
self.radio_status = False self.radio_status = False
@ -85,6 +83,7 @@ class StateManager:
"radio_status": self.radio_status, "radio_status": self.radio_status,
"radio_frequency": self.radio_frequency, "radio_frequency": self.radio_frequency,
"radio_mode": self.radio_mode, "radio_mode": self.radio_mode,
"s_meter_strength": self.s_meter_strength,
"channel_busy_slot": self.channel_busy_slot, "channel_busy_slot": self.channel_busy_slot,
"audio_dbfs": self.audio_dbfs, "audio_dbfs": self.audio_dbfs,
"activities": self.activities_list, "activities": self.activities_list,
@ -119,6 +118,25 @@ class StateManager:
self.arq_irs_sessions[session.id] = session self.arq_irs_sessions[session.id] = session
return True return True
def check_if_running_arq_session(self, irs=False):
sessions = self.arq_irs_sessions if irs else self.arq_iss_sessions
for session_id in sessions:
# do a session cleanup of outdated sessions before
if sessions[session_id].is_session_outdated():
print(f"session cleanup.....{session_id}")
if irs:
self.remove_arq_irs_session(session_id)
else:
self.remove_arq_iss_session(session_id)
# check if ongoing sessions available
if sessions[session_id].state.name not in ['ENDED', 'ABORTED', 'FAILED']:
print(f"[State Manager] running session...[{session_id}]")
return True
return False
def get_arq_iss_session(self, id): def get_arq_iss_session(self, id):
if id not in self.arq_iss_sessions: if id not in self.arq_iss_sessions:
#raise RuntimeError(f"ARQ ISS Session '{id}' not found!") #raise RuntimeError(f"ARQ ISS Session '{id}' not found!")
@ -134,13 +152,11 @@ class StateManager:
return self.arq_irs_sessions[id] return self.arq_irs_sessions[id]
def remove_arq_iss_session(self, id): def remove_arq_iss_session(self, id):
if id not in self.arq_iss_sessions: if id in self.arq_iss_sessions:
raise RuntimeError(f"ARQ ISS Session '{id}' not found!")
del self.arq_iss_sessions[id] del self.arq_iss_sessions[id]
def remove_arq_irs_session(self, id): def remove_arq_irs_session(self, id):
if id not in self.arq_irs_sessions: if id in self.arq_irs_sessions:
raise RuntimeError(f"ARQ ISS Session '{id}' not found!")
del self.arq_irs_sessions[id] del self.arq_irs_sessions[id]
def add_activity(self, activity_data): def add_activity(self, activity_data):
@ -177,3 +193,12 @@ class StateManager:
else: else:
self.channel_busy_condition_codec2 = threading.Event() self.channel_busy_condition_codec2 = threading.Event()
self.calculate_channel_busy_state() self.calculate_channel_busy_state()
def get_radio_status(self):
return {
"radio_status": self.radio_status,
"radio_frequency": self.radio_frequency,
"radio_mode": self.radio_mode,
"radio_rf_level": self.radio_rf_level,
"s_meter_strength": self.s_meter_strength,
}

View file

@ -1,4 +1,5 @@
import sys import sys
import time
sys.path.append('modem') sys.path.append('modem')
import unittest import unittest
@ -17,7 +18,7 @@ import numpy as np
from event_manager import EventManager from event_manager import EventManager
from data_frame_factory import DataFrameFactory from data_frame_factory import DataFrameFactory
import codec2 import codec2
import arq_session_irs
class TestModem: class TestModem:
def __init__(self, event_q): def __init__(self, event_q):
self.data_queue_received = queue.Queue() self.data_queue_received = queue.Queue()
@ -129,12 +130,13 @@ class TestARQSession(unittest.TestCase):
self.establishChannels() self.establishChannels()
params = { params = {
'dxcall': "DJ2LS-3", 'dxcall': "XX1XXX-1",
'data': base64.b64encode(bytes("Hello world!", encoding="utf-8")), 'data': base64.b64encode(bytes("Hello world!", encoding="utf-8")),
} }
cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params) cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params)
cmd.run(self.iss_event_queue, self.iss_modem) cmd.run(self.iss_event_queue, self.iss_modem)
self.waitAndCloseChannels() self.waitAndCloseChannels()
del cmd
def DisabledtestARQSessionLargePayload(self): def DisabledtestARQSessionLargePayload(self):
# set Packet Error Rate (PER) / frame loss probability # set Packet Error Rate (PER) / frame loss probability
@ -142,13 +144,14 @@ class TestARQSession(unittest.TestCase):
self.establishChannels() self.establishChannels()
params = { params = {
'dxcall': "DJ2LS-3", 'dxcall': "XX1XXX-1",
'data': base64.b64encode(np.random.bytes(1000)), 'data': base64.b64encode(np.random.bytes(1000)),
} }
cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params) cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params)
cmd.run(self.iss_event_queue, self.iss_modem) cmd.run(self.iss_event_queue, self.iss_modem)
self.waitAndCloseChannels() self.waitAndCloseChannels()
del cmd
def testARQSessionAbortTransmissionISS(self): def testARQSessionAbortTransmissionISS(self):
# set Packet Error Rate (PER) / frame loss probability # set Packet Error Rate (PER) / frame loss probability
@ -156,7 +159,7 @@ class TestARQSession(unittest.TestCase):
self.establishChannels() self.establishChannels()
params = { params = {
'dxcall': "DJ2LS-3", 'dxcall': "XX1XXX-1",
'data': base64.b64encode(np.random.bytes(100)), 'data': base64.b64encode(np.random.bytes(100)),
} }
cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params) cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params)
@ -167,6 +170,7 @@ class TestARQSession(unittest.TestCase):
self.iss_state_manager.arq_iss_sessions[id].abort_transmission() self.iss_state_manager.arq_iss_sessions[id].abort_transmission()
self.waitAndCloseChannels() self.waitAndCloseChannels()
del cmd
def testARQSessionAbortTransmissionIRS(self): def testARQSessionAbortTransmissionIRS(self):
# set Packet Error Rate (PER) / frame loss probability # set Packet Error Rate (PER) / frame loss probability
@ -174,7 +178,7 @@ class TestARQSession(unittest.TestCase):
self.establishChannels() self.establishChannels()
params = { params = {
'dxcall': "DJ2LS-3", 'dxcall': "XX1XXX-1",
'data': base64.b64encode(np.random.bytes(100)), 'data': base64.b64encode(np.random.bytes(100)),
} }
cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params) cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params)
@ -185,6 +189,42 @@ class TestARQSession(unittest.TestCase):
self.irs_state_manager.arq_irs_sessions[id].abort_transmission() self.irs_state_manager.arq_irs_sessions[id].abort_transmission()
self.waitAndCloseChannels() self.waitAndCloseChannels()
del cmd
def testSessionCleanupISS(self):
params = {
'dxcall': "XX1XXX-1",
'data': base64.b64encode(np.random.bytes(100)),
}
cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params)
cmd.run(self.iss_event_queue, self.iss_modem)
for session_id in self.iss_state_manager.arq_iss_sessions:
session = self.iss_state_manager.arq_iss_sessions[session_id]
ISS_States = session.state_enum
session.state = ISS_States.FAILED
session.session_ended = time.time() - 1000
if session.is_session_outdated():
self.logger.info(f"session [{session_id}] outdated - deleting it")
self.iss_state_manager.remove_arq_iss_session(session_id)
break
del cmd
def testSessionCleanupIRS(self):
session = arq_session_irs.ARQSessionIRS(self.config,
self.irs_modem,
'AA1AAA-1',
random.randint(0, 255))
self.irs_state_manager.register_arq_irs_session(session)
for session_id in self.irs_state_manager.arq_irs_sessions:
session = self.irs_state_manager.arq_irs_sessions[session_id]
irs_States = session.state_enum
session.state = irs_States.FAILED
session.session_ended = time.time() - 1000
if session.is_session_outdated():
self.logger.info(f"session [{session_id}] outdated - deleting it")
self.irs_state_manager.remove_arq_irs_session(session_id)
break
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View file

@ -12,7 +12,7 @@ from command_ping import PingCommand
from command_cq import CQCommand from command_cq import CQCommand
import modem import modem
import frame_handler import frame_handler
from radio_manager import RadioManager
class TestProtocols(unittest.TestCase): class TestProtocols(unittest.TestCase):
@ -27,9 +27,10 @@ class TestProtocols(unittest.TestCase):
cls.event_queue = queue.Queue() cls.event_queue = queue.Queue()
cls.event_manager = EventManager([cls.event_queue]) cls.event_manager = EventManager([cls.event_queue])
cls.radio_manager = RadioManager(cls.config, cls.state_manager, cls.event_manager)
cls.modem_transmit_queue = queue.Queue() cls.modem_transmit_queue = queue.Queue()
cls.modem = modem.RF(cls.config, cls.event_queue, queue.Queue(), queue.Queue(), cls.state_manager) cls.modem = modem.RF(cls.config, cls.event_queue, queue.Queue(), queue.Queue(), cls.state_manager, cls.radio_manager)
modem.TESTMODE = True modem.TESTMODE = True
frame_handler.TESTMODE = True frame_handler.TESTMODE = True

View file

@ -13,7 +13,8 @@ class TestIntegration(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
cmd = "flask --app modem/server run" #cmd = "flask --app modem/server run"
cmd = "python3 modem/server.py"
my_env = os.environ.copy() my_env = os.environ.copy()
my_env["FREEDATA_CONFIG"] = "modem/config.ini.example" my_env["FREEDATA_CONFIG"] = "modem/config.ini.example"
cls.process = Popen(shlex.split(cmd), stdin=PIPE, env=my_env) cls.process = Popen(shlex.split(cmd), stdin=PIPE, env=my_env)

View file

@ -0,0 +1,5 @@
REM Place this batch file in FreeData/modem and then run it
REM ie. c:\FD-Src\modem
python -m pip install -r ..\requirements.txt
pause

10
tools/Modem-Launch.bat Normal file
View file

@ -0,0 +1,10 @@
REM Place this batch file in FreeData/tnc and then run it
REM ie. c:\FD-Src\tnc
REM Set environment variable to let modem know where to find config, change if you need to specify a different config
set FREEDATA_CONFIG=.\config.ini
REM launch modem
flask --app server run
pause

View file

@ -0,0 +1,6 @@
@echo off
REM PLace in modem directory and run to retrieve list of audio devices; you'll need the CRC for the config.ini
python ..\tools\list_audio_devices.py
pause

View file

@ -1,5 +1,8 @@
REM Place this batch file in FreeData/tnc and then run it @echo off
REM ie. c:\FD-Src\gui_vue REM Place this batch file in FreeData/gui and then run it
REM ie. c:\FD-Src\gui
echo Install requirements for GUI...
call npm install call npm install
pause pause

View file

@ -1,5 +1,5 @@
REM Place this batch file in FreeData/tnc and then run it REM Place this batch file in FreeData/gui and then run it
REM ie. c:\FD-Src\gui_vue REM ie. c:\FD-Src\gui
call npm start call npm start
pause pause

View file

@ -1,5 +1,8 @@
REM Place this batch file in FreeData/tnc and then run it @echo off
REM ie. c:\FD-Src\gui_vue REM Place this batch file in FreeData/gui and then run it
REM ie. c:\FD-Src\gui
echo Check for and install updated requirements
call npm update call npm update
pause pause

View file

@ -0,0 +1,5 @@
REM Place this batch file in FreeData/modem and then run it
REM ie. c:\FD-Src\modem
python -m pip install -r ..\requirements.txt
pause

View file

@ -0,0 +1,10 @@
REM Place this batch file in FreeData/modem and then run it
REM ie. c:\FD-Src\modem
REM Set environment variable to let modem know where to find config, change if you need to specify a different config
set FREEDATA_CONFIG=.\config.ini
REM launch modem
python server.py
pause

View file

@ -0,0 +1,6 @@
@echo off
REM PLace in modem directory and run to retrieve list of audio devices; you'll need the CRC for the config.ini
python ..\tools\list_audio_devices.py
pause

View file

@ -1,5 +0,0 @@
REM Place this batch file in FreeData/tnc and then run it
REM ie. c:\FD-Src\tnc
python -m pip install -r ..\requirements.txt
pause

View file

@ -1,5 +0,0 @@
REM Place this batch file in FreeData/tnc and then run it
REM ie. c:\FD-Src\tnc
python daemon.py
pause

View file

@ -1,6 +1,10 @@
@echo off
REM This will copy the helper batch files to the approriate places for you REM This will copy the helper batch files to the approriate places for you
copy GUI* ..\..\gui_vue\ echo Copying GUI scripts to GUI directory
copy TNC* ..\..\tnc\ copy GUI* ..\..\gui\
echo Copying Modem scripts to Modem directory
copy MODEM* ..\..\modem\
pause pause

View file

@ -1,2 +1,2 @@
FREEDATA_CONFIG=modem/config.ini flask --app modem/server run # FREEDATA_CONFIG=modem/config.ini flask --app modem/server run
FREEDATA_CONFIG=modem/config.ini python3 modem/server.py