mirror of
https://github.com/DJ2LS/FreeDATA
synced 2024-05-14 10:04:33 +02:00
Compare commits
170 commits
v0.13.6-al
...
main
Author | SHA1 | Date | |
---|---|---|---|
0e81bddedd | |||
e09b257936 | |||
fbd625cf5b | |||
eccd0e12a7 | |||
c2ff18b111 | |||
83d06259a7 | |||
bea6c086af | |||
b11073521b | |||
329ce9dc20 | |||
b0e46fb998 | |||
33f0d2a160 | |||
7811aae324 | |||
f5d0b90eda | |||
5f1597c27f | |||
bdd8888f1b | |||
e24a64ba25 | |||
ef4ea345fd | |||
ea27044d91 | |||
cb70ed8445 | |||
9fe59ffaa1 | |||
6fb092dd58 | |||
3cb329b379 | |||
8c224ab7dc | |||
577da699a8 | |||
b28ee0aa85 | |||
cc6b3eb958 | |||
ff6ef4b130 | |||
b0c0940e5d | |||
e57d3cf665 | |||
6ed283a5eb | |||
f308134276 | |||
bb8da6f5d8 | |||
aaa2084bfd | |||
9da3fb80f0 | |||
5ed11d771f | |||
adb0c82332 | |||
8d74566b6c | |||
363b90da07 | |||
34747356c9 | |||
03cc026965 | |||
6178454250 | |||
40f7337b6c | |||
0beb4aea23 | |||
6562a44175 | |||
d2b3f3a36e | |||
0f3611fb15 | |||
4d3a0832d5 | |||
24f41edb63 | |||
7714c7aeb6 | |||
6b4bdb4d7d | |||
cbc928f117 | |||
216799fe2b | |||
01b1977630 | |||
41d9642eb1 | |||
787cb2d862 | |||
fbf4e68d9a | |||
791e9ab9c6 | |||
98dada2af5 | |||
4f4c678eac | |||
f5de99a25b | |||
c8fa826e60 | |||
7bed6041f3 | |||
b58749a8a5 | |||
dd703b4bbd | |||
6b7146e02c | |||
2b32fb740c | |||
e4744a113f | |||
9eb3e3f889 | |||
6dc09c4d1b | |||
919b9c6eeb | |||
71c62a2f2c | |||
6d8f81ea89 | |||
de25c95a05 | |||
3a095a00ae | |||
69672482b3 | |||
c4ab8dfe4a | |||
09a87ab9d5 | |||
248bc596cc | |||
e5215a838e | |||
d48f457fd5 | |||
d5eea3c99c | |||
ec6b16b672 | |||
4de818f3a6 | |||
dc67b5632d | |||
6b7327df3d | |||
0bd6f6c9f9 | |||
20b1fe7e2d | |||
1599eb1515 | |||
61e861ee06 | |||
0e07697a98 | |||
11e51afe07 | |||
f63c597e80 | |||
454c4758d0 | |||
184caf57ab | |||
e65e3a984c | |||
e9ee0e600d | |||
dd964474ee | |||
6b693c6759 | |||
d2ee01479a | |||
9520bb4689 | |||
90790bd8e0 | |||
de82f649b1 | |||
d078394af4 | |||
a258f4b16f | |||
eeb72faf83 | |||
0240c6cd1d | |||
7e1d6f6100 | |||
01f3cefe55 | |||
659b1c0c56 | |||
1c6109a25a | |||
f6170604a6 | |||
ffb3db775f | |||
d90ac7cec0 | |||
34dcdd5d8a | |||
bab8aad126 | |||
d4275642d7 | |||
7ff95571c3 | |||
297be826dd | |||
e529bbb395 | |||
1b927ad183 | |||
8081a44e92 | |||
470503eb5c | |||
e221637394 | |||
14cbd46a88 | |||
2ee4776693 | |||
b2657bcbbd | |||
515f895ed3 | |||
6c9439ac70 | |||
2f21dc6abc | |||
2ea6a4fe13 | |||
84504f734f | |||
100c871cd9 | |||
e1b5872e26 | |||
744ed425c3 | |||
2b21aab26b | |||
c369037deb | |||
c8e6e11d84 | |||
05a274edeb | |||
bdaed0e873 | |||
6566412d64 | |||
25de300970 | |||
2f07441fc6 | |||
fd9fb81fa2 | |||
9706260933 | |||
4a34386c26 | |||
419f7732df | |||
59778165bf | |||
0c322bacf8 | |||
e7ce198fa1 | |||
dd4ca1903b | |||
956cede593 | |||
374f400f30 | |||
36890fe131 | |||
d3d09d4019 | |||
f307ed779f | |||
7ecccabcc0 | |||
33ad50fbe2 | |||
3574f76a79 | |||
0100104afb | |||
89f61c15fd | |||
7eb9fa1dc5 | |||
451ec404e9 | |||
e64d71b135 | |||
22f0226600 | |||
836f4b99d8 | |||
c64ea890d1 | |||
6c147106df | |||
6f64b61ea5 | |||
e284a58db9 | |||
8ccff438d0 |
|
@ -14,6 +14,15 @@ jobs:
|
|||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Electron Builder
|
||||
working-directory: gui
|
||||
run: |
|
||||
npm i
|
||||
npm run build
|
||||
|
||||
- name: LIST ALL FILES
|
||||
run: ls -R
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
@ -36,9 +45,9 @@ jobs:
|
|||
run: ls -R
|
||||
|
||||
- name: Create installer
|
||||
uses: joncloud/makensis-action@v4
|
||||
uses: joncloud/makensis-action@v4.1
|
||||
with:
|
||||
script-file: "freedata-server-nsis-config.nsi"
|
||||
script-file: "freedata-nsis-config.nsi"
|
||||
arguments: '/V3'
|
||||
|
||||
- name: LIST ALL FILES
|
||||
|
@ -47,12 +56,14 @@ jobs:
|
|||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: 'FreeData-Server-Installer'
|
||||
path: ./FreeData-Server-Installer.exe
|
||||
name: 'FreeDATA-Installer'
|
||||
path: ./FreeDATA-Installer.exe
|
||||
|
||||
- name: Upload Installer to Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: ./FreeData-Server-Installer.exe
|
||||
files: ./FreeDATA-Installer.exe
|
||||
tag_name: ${{ github.ref_name }}
|
||||
name: 'FreeDATA-Installer-${{ github.ref_name }}'
|
1
.github/workflows/build_server.yml
vendored
1
.github/workflows/build_server.yml
vendored
|
@ -48,6 +48,7 @@ jobs:
|
|||
brew install portaudio
|
||||
python -m pip install --upgrade pip
|
||||
pip3 install pyaudio
|
||||
export PYTHONPATH=/Library/Frameworks/Python.framework/Versions/3.11/lib/:$PYTHONPATH
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
|
|
132
freedata-nsis-config.nsi
Normal file
132
freedata-nsis-config.nsi
Normal file
|
@ -0,0 +1,132 @@
|
|||
!include "MUI2.nsh"
|
||||
|
||||
; Request administrative rights
|
||||
RequestExecutionLevel admin
|
||||
|
||||
; The name and file name of the installer
|
||||
Name "FreeDATA Installer"
|
||||
OutFile "FreeDATA-Installer.exe"
|
||||
|
||||
; Default installation directory for the server
|
||||
InstallDir "$LOCALAPPDATA\FreeDATA"
|
||||
|
||||
; Registry key to store the installation directory
|
||||
InstallDirRegKey HKCU "Software\FreeDATA" "Install_Dir"
|
||||
|
||||
; Modern UI settings
|
||||
!define MUI_ABORTWARNING
|
||||
|
||||
; Installer interface settings
|
||||
!define MUI_ICON "documentation\icon.ico"
|
||||
!define MUI_UNICON "documentation\icon.ico" ; Icon for the uninstaller
|
||||
|
||||
; Define the welcome page text
|
||||
!define MUI_WELCOMEPAGE_TEXT "Welcome to the FreeDATA Setup Wizard. This wizard will guide you through the installation process."
|
||||
!define MUI_FINISHPAGE_TEXT "Folder: $INSTDIR"
|
||||
!define MUI_DIRECTORYPAGE_TEXT_TOP "Please select the installation folder. It's recommended to use the suggested one to avoid permission problems."
|
||||
|
||||
; Pages
|
||||
!insertmacro MUI_PAGE_WELCOME
|
||||
!insertmacro MUI_PAGE_LICENSE "LICENSE"
|
||||
!insertmacro MUI_PAGE_COMPONENTS
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
|
||||
; Uninstaller
|
||||
!insertmacro MUI_UNPAGE_WELCOME
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
!insertmacro MUI_UNPAGE_FINISH
|
||||
|
||||
; Language (you can choose and configure the language(s) you want)
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
|
||||
; Installer Sections
|
||||
Section "FreeData Server" SEC01
|
||||
; Set output path to the installation directory
|
||||
SetOutPath $INSTDIR\freedata-server
|
||||
|
||||
; Check if "config.ini" exists and back it up
|
||||
IfFileExists $INSTDIR\freedata-server\config.ini backupConfig
|
||||
|
||||
doneBackup:
|
||||
; Add your application files here
|
||||
File /r "modem\server.dist\*"
|
||||
|
||||
; Restore the original "config.ini" if it was backed up
|
||||
IfFileExists $INSTDIR\freedata-server\config.ini.bak restoreConfig
|
||||
|
||||
; Create a shortcut in the user's desktop
|
||||
CreateShortCut "$DESKTOP\FreeDATA Server.lnk" "$INSTDIR\freedata-server\freedata-server.exe"
|
||||
|
||||
; Create Uninstaller
|
||||
WriteUninstaller "$INSTDIR\Uninstall.exe"
|
||||
|
||||
; Create a Start Menu directory
|
||||
CreateDirectory "$SMPROGRAMS\FreeDATA"
|
||||
|
||||
; Create shortcut in the Start Menu directory
|
||||
CreateShortCut "$SMPROGRAMS\FreeDATA\FreeDATA Server.lnk" "$INSTDIR\freedata-server\freedata-server.exe"
|
||||
|
||||
; Create an Uninstall shortcut
|
||||
CreateShortCut "$SMPROGRAMS\FreeDATA\Uninstall FreeDATA.lnk" "$INSTDIR\Uninstall.exe"
|
||||
|
||||
|
||||
; Backup "config.ini" before overwriting files
|
||||
backupConfig:
|
||||
Rename $INSTDIR\freedata-server\config.ini $INSTDIR\freedata-server\config.ini.bak
|
||||
Goto doneBackup
|
||||
|
||||
; Restore the original "config.ini"
|
||||
restoreConfig:
|
||||
Delete $INSTDIR\freedata-server\config.ini
|
||||
Rename $INSTDIR\freedata-server\config.ini.bak $INSTDIR\freedata-server\config.ini
|
||||
|
||||
|
||||
|
||||
SectionEnd
|
||||
|
||||
Section "FreeData x64 GUI" SEC02
|
||||
; Set output path to the GUI installation directory
|
||||
SetOutPath $INSTDIR\freedata-gui
|
||||
|
||||
; Add GUI files here
|
||||
File /r "gui\release\win-unpacked\*"
|
||||
|
||||
; Create a shortcut on the desktop for the GUI
|
||||
CreateShortCut "$DESKTOP\FreeDATA GUI.lnk" "$INSTDIR\freedata-gui\freedata.exe"
|
||||
|
||||
; Create a start menu shortcut
|
||||
CreateShortCut "$SMPROGRAMS\FreeDATA\FreeDATA GUI.lnk" "$INSTDIR\freedata-gui\freedata.exe"
|
||||
|
||||
; Create an Uninstall shortcut
|
||||
CreateShortCut "$SMPROGRAMS\FreeDATA\Uninstall FreeDATA.lnk" "$INSTDIR\Uninstall.exe"
|
||||
|
||||
SectionEnd
|
||||
|
||||
; Uninstaller Section
|
||||
Section "Uninstall"
|
||||
; Delete files and directories for the server
|
||||
Delete $INSTDIR\freedata-server\*.*
|
||||
RMDir /r $INSTDIR\freedata-server
|
||||
|
||||
; Delete files and directories for the GUI
|
||||
Delete $INSTDIR\freedata-gui\*.*
|
||||
RMDir /r $INSTDIR\freedata-gui
|
||||
|
||||
; Remove the desktop shortcuts
|
||||
Delete "$DESKTOP\FreeDATA Server.lnk"
|
||||
Delete "$DESKTOP\FreeDATA GUI.lnk"
|
||||
|
||||
; Remove Start Menu shortcuts
|
||||
Delete "$SMPROGRAMS\FreeDATA\*.*"
|
||||
RMDir "$SMPROGRAMS\FreeDATA"
|
||||
|
||||
; Attempt to delete the uninstaller itself
|
||||
Delete $EXEPATH
|
||||
|
||||
; Now remove the installation directory if it's empty
|
||||
RMDir /r $INSTDIR
|
||||
SectionEnd
|
|
@ -1,102 +0,0 @@
|
|||
!include "MUI2.nsh"
|
||||
|
||||
; Request administrative rights
|
||||
RequestExecutionLevel admin
|
||||
|
||||
; The name and file name of the installer
|
||||
Name "FreeData Server"
|
||||
OutFile "FreeData-Server-Installer.exe"
|
||||
|
||||
; Default installation directory
|
||||
; InstallDir "$PROGRAMFILES\FreeData\freedata-server"
|
||||
|
||||
InstallDir "$LOCALAPPDATA\FreeData\freedata-server"
|
||||
|
||||
; Registry key to store the installation directory
|
||||
InstallDirRegKey HKCU "Software\FreeData\freedata-server" "Install_Dir"
|
||||
|
||||
; Modern UI settings
|
||||
!define MUI_ABORTWARNING
|
||||
|
||||
; Installer interface settings
|
||||
!define MUI_ICON "documentation\icon.ico"
|
||||
!define MUI_UNICON "documentation\icon.ico" ; Icon for the uninstaller
|
||||
|
||||
|
||||
; Define the welcome page text
|
||||
!define MUI_WELCOMEPAGE_TEXT "Welcome to the FreeData Server Setup Wizard. This wizard will guide you through the installation process."
|
||||
!define MUI_FINISHPAGE_TEXT "Folder: $INSTDIR"
|
||||
|
||||
|
||||
!define MUI_DIRECTORYPAGE_TEXT_TOP "Please select the installation folder. Its recommended using the suggested one for avoiding permission problems."
|
||||
|
||||
|
||||
; Pages
|
||||
!insertmacro MUI_PAGE_WELCOME
|
||||
!insertmacro MUI_PAGE_LICENSE "LICENSE"
|
||||
;!insertmacro MUI_PAGE_COMPONENTS
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
|
||||
; Uninstaller
|
||||
!insertmacro MUI_UNPAGE_WELCOME
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
!insertmacro MUI_UNPAGE_FINISH
|
||||
|
||||
; Language (you can choose and configure the language(s) you want)
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
; Installer Sections
|
||||
Section "FreeData Server" SEC01
|
||||
|
||||
; Set output path to the installation directory
|
||||
SetOutPath $INSTDIR
|
||||
|
||||
; Check if "config.ini" exists and back it up
|
||||
IfFileExists $INSTDIR\config.ini backupConfig
|
||||
|
||||
doneBackup:
|
||||
; Add your application files here
|
||||
File /r "modem\server.dist\*.*"
|
||||
|
||||
; Restore the original "config.ini" if it was backed up
|
||||
IfFileExists $INSTDIR\config.ini.bak restoreConfig
|
||||
|
||||
|
||||
|
||||
; Create a shortcut in the user's desktop
|
||||
CreateShortCut "$DESKTOP\FreeData Server.lnk" "$INSTDIR\freedata-server.exe"
|
||||
|
||||
; Create Uninstaller
|
||||
WriteUninstaller "$INSTDIR\Uninstall.exe"
|
||||
|
||||
; Backup "config.ini" before overwriting files
|
||||
backupConfig:
|
||||
Rename $INSTDIR\config.ini $INSTDIR\config.ini.bak
|
||||
Goto doneBackup
|
||||
|
||||
; Restore the original "config.ini"
|
||||
restoreConfig:
|
||||
Delete $INSTDIR\config.ini
|
||||
Rename $INSTDIR\config.ini.bak $INSTDIR\config.ini
|
||||
|
||||
|
||||
SectionEnd
|
||||
|
||||
; Uninstaller Section
|
||||
Section "Uninstall"
|
||||
|
||||
; Delete files and directories
|
||||
Delete $INSTDIR\freedata-server.exe
|
||||
RMDir /r $INSTDIR
|
||||
|
||||
; Remove the shortcut
|
||||
Delete "$DESKTOP\FreeData Server.lnk"
|
||||
|
||||
; Additional uninstallation commands here
|
||||
|
||||
SectionEnd
|
||||
|
||||
|
|
@ -45,18 +45,12 @@
|
|||
"icon": "build/icon.png",
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
"target": "portable",
|
||||
"arch": ["arm64", "x64"]
|
||||
}
|
||||
],
|
||||
"artifactName": "${productName}-GUI-Windows-${version}.${ext}"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"perMachine": false,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"deleteAppDataOnUninstall": true
|
||||
},
|
||||
"linux": {
|
||||
"category": "Development",
|
||||
"target": [
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { app, BrowserWindow, shell, ipcMain } from "electron";
|
||||
import { release, platform } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
import { release, platform } from "os";
|
||||
import { join, dirname } from "path";
|
||||
import { existsSync } from "fs";
|
||||
import { spawn } from "child_process";
|
||||
|
||||
|
@ -20,7 +19,6 @@ process.env.DIST = join(process.env.DIST_ELECTRON, "../dist");
|
|||
process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL
|
||||
? join(process.env.DIST_ELECTRON, "../public")
|
||||
: process.env.DIST;
|
||||
process.env.FDUpdateAvail = "0";
|
||||
|
||||
// Disable GPU Acceleration for Windows 7
|
||||
if (release().startsWith("6.1")) app.disableHardwareAcceleration();
|
||||
|
@ -40,7 +38,7 @@ if (!app.requestSingleInstanceLock()) {
|
|||
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
|
||||
|
||||
// set daemon process var
|
||||
var daemonProcess = null;
|
||||
var serverProcess = null;
|
||||
let win: BrowserWindow | null = null;
|
||||
// Here, you can also use other preload
|
||||
const preload = join(__dirname, "../preload/index.js");
|
||||
|
@ -75,9 +73,9 @@ async function createWindow() {
|
|||
}
|
||||
|
||||
// Test actively push message to the Electron-Renderer
|
||||
win.webContents.on("did-finish-load", () => {
|
||||
win?.webContents.send("main-process-message", new Date().toLocaleString());
|
||||
});
|
||||
//win.webContents.on("did-finish-load", () => {
|
||||
// win?.webContents.send("main-process-message", new Date().toLocaleString());
|
||||
//});
|
||||
|
||||
// Make all links open with the browser, not with the application
|
||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||
|
@ -87,12 +85,7 @@ async function createWindow() {
|
|||
// win.webContents.on('will-navigate', (event, url) => { }) #344
|
||||
|
||||
win.once("ready-to-show", () => {
|
||||
//autoUpdater.logger = log.scope("updater");
|
||||
//autoUpdater.channel = config.update_channel;
|
||||
autoUpdater.autoInstallOnAppQuit = false;
|
||||
autoUpdater.autoDownload = true;
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
//autoUpdater.quitAndInstall();
|
||||
//
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -102,68 +95,61 @@ app.whenReady().then(() => {
|
|||
|
||||
console.log(platform());
|
||||
//Generate daemon binary path
|
||||
var daemonPath = "";
|
||||
var serverPath = "";
|
||||
console.log(process.env);
|
||||
|
||||
// Attempt to find Installation Folder
|
||||
console.log(app.getAppPath());
|
||||
console.log(join(app.getAppPath(), "..", ".."));
|
||||
console.log(join(app.getAppPath(), "..", "..", ".."));
|
||||
|
||||
//var basePath = join(app.getAppPath(), '..', '..', '..') || join(process.env.PWD, '..') || join(process.env.INIT_CWD, '..') || join(process.env.DIST, '..', '..', '..');
|
||||
var basePath = join(app.getAppPath(), "..", "..", "..");
|
||||
switch (platform().toLowerCase()) {
|
||||
case "darwin":
|
||||
daemonPath = join(process.resourcesPath, "modem", "freedata-server");
|
||||
case "linux":
|
||||
daemonPath = join(process.resourcesPath, "modem", "freedata-server");
|
||||
break;
|
||||
//case "darwin":
|
||||
//serverPath = join(basePath, "freedata-server", "freedata-server.exe");
|
||||
//serverProcess = spawn(serverPath, [], { detached: true });
|
||||
//serverProcess.unref(); // Allow the server process to continue running independently of the parent process
|
||||
// break;
|
||||
//case "linux":
|
||||
//serverPath = join(basePath, "freedata-server", "freedata-server.exe");
|
||||
//serverProcess = spawn(serverPath, [], { detached: true });
|
||||
//serverProcess.unref(); // Allow the server process to continue running independently of the parent process
|
||||
// break;
|
||||
case "win32":
|
||||
daemonPath = join(process.resourcesPath, "modem", "freedata-server.exe");
|
||||
break;
|
||||
case "win64":
|
||||
daemonPath = join(process.resourcesPath, "modem", "freedata-server.exe");
|
||||
serverPath = join(basePath, "freedata-server", "freedata-server.exe");
|
||||
console.log(`Starting server with path: ${serverPath}`);
|
||||
serverProcess = spawn(
|
||||
"cmd.exe",
|
||||
["/c", "start", "cmd.exe", "/c", serverPath],
|
||||
{ shell: true },
|
||||
);
|
||||
console.log(`Started server | PID: ${serverProcess.pid}`);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log("Unhandled OS Platform: ", platform());
|
||||
serverProcess = null;
|
||||
serverPath = null;
|
||||
break;
|
||||
}
|
||||
|
||||
//Start daemon binary if it exists
|
||||
if (existsSync(daemonPath)) {
|
||||
console.log("Starting freedata-server binary");
|
||||
console.log("daemonPath:", daemonPath);
|
||||
console.log("CWD:", join(daemonPath, ".."));
|
||||
/*
|
||||
var daemonProcess = spawn("freedata-server", [], {
|
||||
cwd: join(process.env.DIST, "modem"),
|
||||
shell: true
|
||||
});
|
||||
*/
|
||||
/*
|
||||
daemonProcess = spawn(daemonPath, [], {
|
||||
shell: true
|
||||
});
|
||||
console.log(daemonProcess)
|
||||
*/
|
||||
daemonProcess = spawn(daemonPath, [], {});
|
||||
serverProcess.on("error", (err) => {
|
||||
console.error("Failed to start server process:", err);
|
||||
serverProcess = null;
|
||||
serverPath = null;
|
||||
});
|
||||
serverProcess.stdout.on("data", (data) => {
|
||||
//console.log(`stdout: ${data}`);
|
||||
});
|
||||
|
||||
// return process messages
|
||||
daemonProcess.on("error", (err) => {
|
||||
//daemonProcessLog.error(`error when starting daemon: ${err}`);
|
||||
console.log(err);
|
||||
});
|
||||
daemonProcess.on("message", () => {
|
||||
// daemonProcessLog.info(`${data}`);
|
||||
});
|
||||
daemonProcess.stdout.on("data", () => {
|
||||
// daemonProcessLog.info(`${data}`);
|
||||
});
|
||||
daemonProcess.stderr.on("data", (data) => {
|
||||
// daemonProcessLog.info(`${data}`);
|
||||
console.log(data);
|
||||
});
|
||||
daemonProcess.on("close", (code) => {
|
||||
// daemonProcessLog.warn(`daemonProcess exited with code ${code}`);
|
||||
});
|
||||
} else {
|
||||
daemonProcess = null;
|
||||
daemonPath = null;
|
||||
console.log("Daemon binary doesn't exist--normal for dev environments.");
|
||||
}
|
||||
serverProcess.stderr.on("data", (data) => {
|
||||
console.error(`stderr: ${data}`);
|
||||
});
|
||||
});
|
||||
|
||||
//)
|
||||
app.on("before-quit", () => {
|
||||
close_sub_processes();
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
|
@ -205,104 +191,33 @@ ipcMain.handle("open-win", (_, arg) => {
|
|||
}
|
||||
});
|
||||
|
||||
//restart and install udpate
|
||||
ipcMain.on("request-restart-and-install-update", (event, data) => {
|
||||
close_sub_processes();
|
||||
autoUpdater.quitAndInstall();
|
||||
});
|
||||
|
||||
// LISTENER FOR UPDATER EVENTS
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
process.env.FDUpdateAvail = "1";
|
||||
console.log("update available");
|
||||
|
||||
let arg = {
|
||||
status: "update-available",
|
||||
info: info,
|
||||
};
|
||||
win.webContents.send("action-updater", arg);
|
||||
});
|
||||
|
||||
autoUpdater.on("update-not-available", (info) => {
|
||||
console.log("update not available");
|
||||
let arg = {
|
||||
status: "update-not-available",
|
||||
info: info,
|
||||
};
|
||||
win.webContents.send("action-updater", arg);
|
||||
});
|
||||
|
||||
autoUpdater.on("update-downloaded", (info) => {
|
||||
process.env.FDUpdateAvail = "1";
|
||||
console.log("update downloaded");
|
||||
let arg = {
|
||||
status: "update-downloaded",
|
||||
info: info,
|
||||
};
|
||||
win.webContents.send("action-updater", arg);
|
||||
// we need to call this at this point.
|
||||
// if an update is available and we are force closing the app
|
||||
// the entire screen crashes...
|
||||
//console.log('quit application and install update');
|
||||
//autoUpdater.quitAndInstall();
|
||||
});
|
||||
|
||||
autoUpdater.on("checking-for-update", () => {
|
||||
console.log("checking for update");
|
||||
let arg = {
|
||||
status: "checking-for-update",
|
||||
version: app.getVersion(),
|
||||
};
|
||||
win.webContents.send("action-updater", arg);
|
||||
});
|
||||
|
||||
autoUpdater.on("download-progress", (progress) => {
|
||||
let arg = {
|
||||
status: "download-progress",
|
||||
progress: progress,
|
||||
};
|
||||
win.webContents.send("action-updater", arg);
|
||||
});
|
||||
|
||||
autoUpdater.on("error", (error) => {
|
||||
console.log("update error");
|
||||
let arg = {
|
||||
status: "error",
|
||||
progress: error,
|
||||
};
|
||||
win.webContents.send("action-updater", arg);
|
||||
console.log("AUTO UPDATER : " + error);
|
||||
});
|
||||
|
||||
function close_sub_processes() {
|
||||
console.log("closing sub processes");
|
||||
console.log("Closing sub processes...");
|
||||
|
||||
// closing the modem binary if not closed when closing application and also our daemon which has been started by the gui
|
||||
try {
|
||||
if (daemonProcess != null) {
|
||||
daemonProcess.kill();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
if (serverProcess != null) {
|
||||
try {
|
||||
console.log(`Killing server process with PID: ${serverProcess.pid}`);
|
||||
|
||||
console.log("closing modem and daemon");
|
||||
try {
|
||||
if (platform() == "win32") {
|
||||
spawn("Taskkill", ["/IM", "freedata-modem.exe", "/F"]);
|
||||
spawn("Taskkill", ["/IM", "freedata-server.exe", "/F"]);
|
||||
}
|
||||
switch (platform().toLowerCase()) {
|
||||
//case "darwin":
|
||||
// process.kill(serverProcess.pid);
|
||||
// break;
|
||||
//case "linux":
|
||||
// process.kill(serverProcess.pid);
|
||||
// break;
|
||||
case "win32":
|
||||
// For Windows, use taskkill to ensure all child processes are also terminated
|
||||
spawn("taskkill", ["/pid", serverProcess.pid.toString(), "/f", "/t"]);
|
||||
break;
|
||||
|
||||
if (platform() == "linux") {
|
||||
spawn("pkill", ["-9", "freedata-modem"]);
|
||||
spawn("pkill", ["-9", "freedata-server"]);
|
||||
default:
|
||||
console.log("Unhandled OS Platform: ", platform());
|
||||
serverProcess = null;
|
||||
serverPath = null;
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error killing server process: ${error}`);
|
||||
}
|
||||
|
||||
if (platform() == "darwin") {
|
||||
spawn("pkill", ["-9", "freedata-modem"]);
|
||||
spawn("pkill", ["-9", "freedata-server"]);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,83 +111,4 @@ window.onmessage = (ev) => {
|
|||
ev.data.payload === "removeLoading" && removeLoading();
|
||||
};
|
||||
|
||||
setTimeout(removeLoading, 4999);
|
||||
|
||||
// IPC ACTION FOR AUTO UPDATER
|
||||
ipcRenderer.on("action-updater", (event, arg) => {
|
||||
if (arg.status == "download-progress") {
|
||||
var progressinfo =
|
||||
"(" +
|
||||
Math.round(arg.progress.transferred / 1024) +
|
||||
"kB /" +
|
||||
Math.round(arg.progress.total / 1024) +
|
||||
"kB)" +
|
||||
" @ " +
|
||||
Math.round(arg.progress.bytesPerSecond / 1024) +
|
||||
"kByte/s";
|
||||
document.getElementById("UpdateProgressInfo").innerHTML = progressinfo;
|
||||
|
||||
document
|
||||
.getElementById("UpdateProgressBar")
|
||||
.setAttribute("aria-valuenow", arg.progress.percent);
|
||||
document
|
||||
.getElementById("UpdateProgressBar")
|
||||
.setAttribute("style", "width:" + arg.progress.percent + "%;");
|
||||
}
|
||||
|
||||
if (arg.status == "checking-for-update") {
|
||||
//document.title = document.title + ' - v' + arg.version;
|
||||
//updateTitle(
|
||||
// config.myCall,
|
||||
// config.tnc_host,
|
||||
// config.tnc_port,
|
||||
// " -v " + arg.version,
|
||||
//);
|
||||
document.getElementById("updater_status").innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>';
|
||||
|
||||
document.getElementById("updater_status").className =
|
||||
"btn btn-secondary btn-sm";
|
||||
document.getElementById("update_and_install").style.display = "none";
|
||||
}
|
||||
if (arg.status == "update-downloaded") {
|
||||
document.getElementById("update_and_install").removeAttribute("style");
|
||||
document.getElementById("updater_status").innerHTML =
|
||||
'<i class="bi bi-cloud-download ms-1 me-1" style="color: white;"></i>';
|
||||
document.getElementById("updater_status").className =
|
||||
"btn btn-success btn-sm";
|
||||
|
||||
// HERE WE NEED TO RUN THIS SOMEHOW...
|
||||
//mainLog.info('quit application and install update');
|
||||
//autoUpdater.quitAndInstall();
|
||||
}
|
||||
if (arg.status == "update-not-available") {
|
||||
document.getElementById("updater_last_version").innerHTML =
|
||||
arg.info.releaseName;
|
||||
document.getElementById("updater_last_update").innerHTML =
|
||||
arg.info.releaseDate;
|
||||
document.getElementById("updater_release_notes").innerHTML =
|
||||
arg.info.releaseNotes;
|
||||
|
||||
document.getElementById("updater_status").innerHTML =
|
||||
'<i class="bi bi-check2-square ms-1 me-1" style="color: white;"></i>';
|
||||
document.getElementById("updater_status").className =
|
||||
"btn btn-success btn-sm";
|
||||
document.getElementById("update_and_install").style.display = "none";
|
||||
}
|
||||
if (arg.status == "update-available") {
|
||||
document.getElementById("updater_status").innerHTML =
|
||||
'<i class="bi bi-hourglass-split ms-1 me-1" style="color: white;"></i>';
|
||||
document.getElementById("updater_status").className =
|
||||
"btn btn-warning btn-sm";
|
||||
document.getElementById("update_and_install").style.display = "none";
|
||||
}
|
||||
|
||||
if (arg.status == "error") {
|
||||
document.getElementById("updater_status").innerHTML =
|
||||
'<i class="bi bi-exclamation-square ms-1 me-1" style="color: white;"></i>';
|
||||
document.getElementById("updater_status").className =
|
||||
"btn btn-danger btn-sm";
|
||||
document.getElementById("update_and_install").style.display = "none";
|
||||
}
|
||||
});
|
||||
setTimeout(removeLoading, 3999);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"name": "FreeDATA",
|
||||
"description": "FreeDATA Client application for connecting to FreeDATA server",
|
||||
"private": true,
|
||||
"version": "0.13.6-alpha",
|
||||
"version": "0.14.5-alpha",
|
||||
"main": "dist-electron/main/index.js",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
|
@ -13,7 +13,8 @@
|
|||
"release": "vue-tsc --noEmit && vite build && electron-builder -p onTag",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint --ext .js,.vue src",
|
||||
"lint-fix": "eslint --ext .js,.vue --fix src"
|
||||
"lint-fix": "eslint --ext .js,.vue --fix src",
|
||||
"install-deps": "npm install && npm update"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -39,12 +40,10 @@
|
|||
"blob-util": "2.0.2",
|
||||
"bootstrap": "5.3.2",
|
||||
"bootstrap-icons": "1.11.3",
|
||||
"bootswatch": "5.3.2",
|
||||
"browser-image-compression": "2.0.2",
|
||||
"chart.js": "4.4.1",
|
||||
"chartjs-plugin-annotation": "3.0.1",
|
||||
"electron-log": "5.1.1",
|
||||
"electron-updater": "6.1.7",
|
||||
"emoji-picker-element": "1.21.0",
|
||||
"emoji-picker-element-data": "1.6.0",
|
||||
"file-saver": "2.0.5",
|
||||
|
@ -56,7 +55,7 @@
|
|||
"qth-locator": "2.1.0",
|
||||
"socket.io": "4.7.4",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "3.4.15",
|
||||
"vue": "3.4.21",
|
||||
"vue-chartjs": "5.3.0",
|
||||
"vuemoji-picker": "0.2.0"
|
||||
},
|
||||
|
@ -64,7 +63,7 @@
|
|||
"@types/nconf": "^0.10.6",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"electron": "28.2.2",
|
||||
"electron": "28.2.6",
|
||||
"electron-builder": "24.9.1",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
|
@ -73,13 +72,13 @@
|
|||
"eslint-plugin-n": "16.6.2",
|
||||
"eslint-plugin-prettier": "5.1.3",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-vue": "9.20.1",
|
||||
"eslint-plugin-vue": "9.22.0",
|
||||
"typescript": "5.3.3",
|
||||
"vite": "5.1.3",
|
||||
"vite": "5.1.7",
|
||||
"vite-plugin-electron": "0.28.2",
|
||||
"vite-plugin-electron-renderer": "0.14.5",
|
||||
"vitest": "1.2.2",
|
||||
"vue": "3.4.15",
|
||||
"vitest": "1.3.1",
|
||||
"vue": "3.4.21",
|
||||
"vue-tsc": "1.8.27"
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 125 KiB |
Binary file not shown.
Before Width: | Height: | Size: 58 KiB |
Binary file not shown.
Before Width: | Height: | Size: 342 KiB |
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-fill" viewBox="0 0 16 16">
|
||||
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3Zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 225 B |
Binary file not shown.
Before Width: | Height: | Size: 136 KiB |
|
@ -25,7 +25,7 @@ import {
|
|||
} from "chart.js";
|
||||
|
||||
import { Bar } from "vue-chartjs";
|
||||
import { ref, computed } from "vue";
|
||||
import { watch, nextTick, ref, computed } from "vue";
|
||||
import annotationPlugin from "chartjs-plugin-annotation";
|
||||
|
||||
ChartJS.register(
|
||||
|
@ -101,6 +101,20 @@ const beaconHistogramData = computed(() => ({
|
|||
},
|
||||
],
|
||||
}));
|
||||
|
||||
const messagesContainer = ref(null);
|
||||
watch(
|
||||
() => chat.scrollTrigger,
|
||||
(newVal, oldVal) => {
|
||||
//console.log("Trigger changed from", oldVal, "to", newVal); // Debugging line
|
||||
nextTick(() => {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop =
|
||||
messagesContainer.value.scrollHeight;
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -143,7 +157,7 @@ const beaconHistogramData = computed(() => ({
|
|||
</nav>
|
||||
|
||||
<!-- Chat Messages Area -->
|
||||
<div class="flex-grow-1 overflow-auto">
|
||||
<div class="flex-grow-1 overflow-auto" ref="messagesContainer">
|
||||
<chat_messages />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -13,11 +13,7 @@ const chat = useChatStore(pinia);
|
|||
function chatSelected(callsign) {
|
||||
chat.selectedCallsign = callsign.toUpperCase();
|
||||
// scroll message container to bottom
|
||||
var messageBody = document.getElementById("message-container");
|
||||
if (messageBody != null) {
|
||||
// needs sensible defaults
|
||||
messageBody.scrollTop = messageBody.scrollHeight - messageBody.clientHeight;
|
||||
}
|
||||
chat.triggerScrollToBottom();
|
||||
|
||||
processBeaconData(callsign);
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ import {
|
|||
Legend
|
||||
} from 'chart.js'
|
||||
import { Line } from 'vue-chartjs'
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, nextTick } from 'vue';
|
||||
|
||||
|
||||
import { VuemojiPicker, EmojiClickEventDetail } from 'vuemoji-picker'
|
||||
|
@ -90,6 +90,8 @@ function transmitNewMessage() {
|
|||
chat.selectedCallsign = Object.keys(chat.callsign_list)[0];
|
||||
}
|
||||
|
||||
|
||||
|
||||
chat.inputText = chat.inputText.trim();
|
||||
|
||||
// Proceed only if there is text or files selected
|
||||
|
@ -101,6 +103,7 @@ function transmitNewMessage() {
|
|||
type: file.type,
|
||||
data: file.content
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
if (chat.selectedCallsign.startsWith("BC-")) {
|
||||
|
@ -120,6 +123,7 @@ function transmitNewMessage() {
|
|||
chat.inputText = '';
|
||||
chatModuleMessage.value = "";
|
||||
resetFile()
|
||||
|
||||
}
|
||||
|
||||
function resetFile(event){
|
||||
|
|
|
@ -485,8 +485,8 @@ function quickfill() {
|
|||
<i class="bi bi-grip-vertical h5"></i>
|
||||
</button>
|
||||
|
||||
<div class="grid-container" style="height: calc(100vh - 51px);">
|
||||
<div class="grid-stack">
|
||||
<div class="grid-container z-0" style="height: calc(100vh - 51px);">
|
||||
<div class="grid-stack z-0">
|
||||
<div
|
||||
v-for="(w, indexs) in items"
|
||||
class="grid-stack-item"
|
||||
|
@ -832,7 +832,7 @@ function quickfill() {
|
|||
<h6>15m</h6>
|
||||
</div>
|
||||
</a>
|
||||
<a href="#" class="list-group-item list-group-item-action" @click="updateFrequencyAndApply(14093000)">
|
||||
<a href="#" class="list-group-item list-group-item-action" @click="updateFrequencyAndApply(18106000)">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1">18.106 MHz</h5>
|
||||
<small>EU / US</small>
|
||||
|
|
|
@ -21,6 +21,14 @@ function startStopBeacon() {
|
|||
}
|
||||
}
|
||||
var dxcallPing = ref("");
|
||||
window.addEventListener(
|
||||
"stationSelected",
|
||||
function (eventdata) {
|
||||
let evt = <CustomEvent>eventdata;
|
||||
dxcallPing.value = evt.detail;
|
||||
},
|
||||
false,
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="card h-100">
|
||||
|
|
|
@ -21,6 +21,14 @@ function startStopBeacon() {
|
|||
}
|
||||
}
|
||||
var dxcallPing = ref("");
|
||||
window.addEventListener(
|
||||
"stationSelected",
|
||||
function (eventdata) {
|
||||
let evt = <CustomEvent>eventdata;
|
||||
dxcallPing.value = evt.detail;
|
||||
},
|
||||
false,
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="card h-100">
|
||||
|
|
|
@ -34,6 +34,10 @@ function getMaidenheadDistance(dxGrid) {
|
|||
//
|
||||
}
|
||||
}
|
||||
function pushToPing(origin)
|
||||
{
|
||||
window.dispatchEvent(new CustomEvent("stationSelected", {bubbles:true, detail: origin }));
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="card h-100">
|
||||
|
@ -61,7 +65,7 @@ function getMaidenheadDistance(dxGrid) {
|
|||
</thead>
|
||||
<tbody id="gridHeardStations">
|
||||
<!--https://vuejs.org/guide/essentials/list.html-->
|
||||
<tr v-for="item in state.heard_stations" :key="item.origin">
|
||||
<tr v-for="item in state.heard_stations" :key="item.origin" @click="pushToPing(item.origin)">
|
||||
<td>
|
||||
{{ getDateTime(item.timestamp) }}
|
||||
</td>
|
||||
|
|
|
@ -34,6 +34,10 @@ function getMaidenheadDistance(dxGrid) {
|
|||
//
|
||||
}
|
||||
}
|
||||
function pushToPing(origin)
|
||||
{
|
||||
window.dispatchEvent(new CustomEvent("stationSelected", {bubbles:true, detail: origin }));
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="card h-100">
|
||||
|
@ -54,7 +58,7 @@ function getMaidenheadDistance(dxGrid) {
|
|||
</thead>
|
||||
<tbody id="miniHeardStations">
|
||||
<!--https://vuejs.org/guide/essentials/list.html-->
|
||||
<tr v-for="item in state.heard_stations" :key="item.origin">
|
||||
<tr v-for="item in state.heard_stations" :key="item.origin" @click="pushToPing(item.origin)">
|
||||
<td>
|
||||
<span class="fs-6">{{ getDateTime(item.timestamp) }}</span>
|
||||
</td>
|
||||
|
|
|
@ -12,6 +12,15 @@ function transmitPing() {
|
|||
sendModemPing(dxcallPing.value.toUpperCase());
|
||||
}
|
||||
var dxcallPing = ref("");
|
||||
|
||||
window.addEventListener(
|
||||
"stationSelected",
|
||||
function (eventdata) {
|
||||
let evt = <CustomEvent>eventdata;
|
||||
dxcallPing.value = evt.detail;
|
||||
},
|
||||
false,
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="input-group" style="width: calc(100% - 24px)">
|
||||
|
|
|
@ -1,223 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
|
||||
import { setActivePinia } from "pinia";
|
||||
import pinia from "../store/index";
|
||||
setActivePinia(pinia);
|
||||
|
||||
import infoScreen_updater from "./infoScreen_updater.vue";
|
||||
|
||||
function openWebExternal(url) {
|
||||
open(url);
|
||||
}
|
||||
const cards = ref([
|
||||
{
|
||||
titleName: "Simon",
|
||||
titleCall: "DJ2LS",
|
||||
role: "Founder & Core Developer",
|
||||
imgSrc: "dj2ls.png",
|
||||
},
|
||||
{
|
||||
titleName: "Alan",
|
||||
titleCall: "N1QM",
|
||||
role: "Developer",
|
||||
imgSrc: "person-fill.svg",
|
||||
},
|
||||
{
|
||||
titleName: "Stefan",
|
||||
titleCall: "DK5SM",
|
||||
role: "Tester",
|
||||
imgSrc: "person-fill.svg",
|
||||
},
|
||||
{
|
||||
titleName: "Wolfgang",
|
||||
titleCall: "DL4IAZ",
|
||||
role: "Supporter",
|
||||
imgSrc: "person-fill.svg",
|
||||
},
|
||||
{
|
||||
titleName: "David",
|
||||
titleCall: "VK5DGR",
|
||||
role: "Codec 2 Founder",
|
||||
imgSrc: "vk5dgr.jpeg",
|
||||
},
|
||||
{
|
||||
titleName: "John",
|
||||
titleCall: "EI7IG",
|
||||
role: "Tester",
|
||||
imgSrc: "ei7ig.jpeg",
|
||||
},
|
||||
{
|
||||
titleName: "Paul",
|
||||
titleCall: "N2KIQ",
|
||||
role: "Developer",
|
||||
imgSrc: "person-fill.svg",
|
||||
},
|
||||
{
|
||||
titleName: "Trip",
|
||||
titleCall: "KT4WO",
|
||||
role: "Tester",
|
||||
imgSrc: "kt4wo.png",
|
||||
},
|
||||
{
|
||||
titleName: "Manuel",
|
||||
titleCall: "DF7MH",
|
||||
role: "Tester",
|
||||
imgSrc: "person-fill.svg",
|
||||
},
|
||||
{
|
||||
titleName: "Darren",
|
||||
titleCall: "G0HWW",
|
||||
role: "Tester",
|
||||
imgSrc: "person-fill.svg",
|
||||
},
|
||||
{
|
||||
titleName: "Kai",
|
||||
titleCall: "LA3QMA",
|
||||
role: "Developer",
|
||||
imgSrc: "person-fill.svg",
|
||||
},
|
||||
{
|
||||
titleName: "Pedro",
|
||||
titleCall: "F4JAW",
|
||||
role: "Core Developer",
|
||||
imgSrc: "person-fill.svg",
|
||||
},
|
||||
]);
|
||||
|
||||
// Shuffle cards
|
||||
function shuffleCards() {
|
||||
cards.value = cards.value.sort(() => Math.random() - 0.5);
|
||||
}
|
||||
|
||||
onMounted(shuffleCards);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!--<infoScreen_updater />-->
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<h6>Important URLs</h6>
|
||||
|
||||
<div
|
||||
class="btn-toolbar mx-auto"
|
||||
role="toolbar"
|
||||
aria-label="Toolbar with button groups"
|
||||
>
|
||||
<div class="btn-group">
|
||||
<button
|
||||
class="btn btn-sm bi bi-geo-alt btn-secondary me-2"
|
||||
id="openExplorer"
|
||||
type="button"
|
||||
data-bs-placement="bottom"
|
||||
@click="openWebExternal('https://explorer.freedata.app')"
|
||||
>
|
||||
Explorer map
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button
|
||||
class="btn btn-sm btn-secondary me-2 bi bi-graph-up"
|
||||
id="btnStats"
|
||||
type="button"
|
||||
data-bs-placement="bottom"
|
||||
@click="openWebExternal('https://statistics.freedata.app/')"
|
||||
>
|
||||
Statistics
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button
|
||||
class="btn btn-secondary bi bi-bookmarks me-2"
|
||||
id="fdWww"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-trigger="hover"
|
||||
title="FreeDATA website"
|
||||
role="button"
|
||||
@click="openWebExternal('https://freedata.app')"
|
||||
>
|
||||
Website
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button
|
||||
class="btn btn-secondary bi bi-github me-2"
|
||||
id="ghUrl"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-trigger="hover"
|
||||
title="Github"
|
||||
role="button"
|
||||
@click="openWebExternal('https://github.com/dj2ls/freedata')"
|
||||
>
|
||||
Github
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button
|
||||
class="btn btn-secondary bi bi-wikipedia me-2"
|
||||
id="wikiUrl"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-trigger="hover"
|
||||
title="Wiki"
|
||||
role="button"
|
||||
@click="openWebExternal('https://wiki.freedata.app')"
|
||||
>
|
||||
Wiki
|
||||
</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button
|
||||
class="btn btn-secondary bi bi-discord"
|
||||
id="discordUrl"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-trigger="hover"
|
||||
title="Discord"
|
||||
role="button"
|
||||
@click="openWebExternal('https://discord.freedata.app')"
|
||||
>
|
||||
Discord
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="row">
|
||||
<h6>We would like to especially thank the following</h6>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="d-flex flex-nowrap overflow-y-auto w-100"
|
||||
style="height: calc(100vh - 170px); overflow-x: hidden"
|
||||
>
|
||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 row-cols-lg-6">
|
||||
<div class="d-inline-block" v-for="card in cards" :key="card.titleName">
|
||||
<div class="col">
|
||||
<div class="card border-dark m-1" style="max-width: 10rem">
|
||||
<img :src="card.imgSrc" class="card-img-top grayscale" />
|
||||
<div class="card-body">
|
||||
<p class="card-text text-center">{{ card.role }}</p>
|
||||
</div>
|
||||
<div class="card-footer text-body-secondary text-center">
|
||||
<strong>{{ card.titleCall }}</strong>
|
||||
</div>
|
||||
<div class="card-footer text-body-secondary text-center">
|
||||
<strong>{{ card.titleName }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.grayscale {
|
||||
filter: grayscale(100%);
|
||||
transition: filter 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.grayscale:hover {
|
||||
filter: grayscale(0);
|
||||
}
|
||||
</style>
|
|
@ -5,31 +5,21 @@ setActivePinia(pinia);
|
|||
|
||||
import main_modals from "./main_modals.vue";
|
||||
import main_top_navbar from "./main_top_navbar.vue";
|
||||
import main_rig_control from "./main_rig_control.vue";
|
||||
import settings_view from "./settings.vue";
|
||||
import main_active_rig_control from "./main_active_rig_control.vue";
|
||||
import main_footer_navbar from "./main_footer_navbar.vue";
|
||||
|
||||
import main_active_stats from "./main_active_stats.vue";
|
||||
import main_active_broadcasts from "./main_active_broadcasts.vue";
|
||||
import main_active_heard_stations from "./main_active_heard_stations.vue";
|
||||
import main_active_audio_level from "./main_active_audio_level.vue";
|
||||
|
||||
import chat from "./chat.vue";
|
||||
import infoScreen from "./infoScreen.vue";
|
||||
import main_modem_healthcheck from "./main_modem_healthcheck.vue";
|
||||
import Dynamic_components from "./dynamic_components.vue";
|
||||
|
||||
import { getFreedataMessages } from "../js/api";
|
||||
import { getRemote } from "../store/settingsStore.js";
|
||||
import { loadAllData } from "../js/eventHandler";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-------------------------------- INFO TOASTS ---------------->
|
||||
<div
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
class="position-relative z-3"
|
||||
>
|
||||
<div aria-live="polite" aria-atomic="true" class="position-relative z-3">
|
||||
<div
|
||||
class="toast-container position-absolute top-0 end-0 p-3"
|
||||
id="mainToastContainer"
|
||||
|
@ -47,6 +37,7 @@ import { getFreedataMessages } from "../js/api";
|
|||
id="main-list-tab"
|
||||
role="tablist"
|
||||
style="margin-top: 100px"
|
||||
@click="loadAllData"
|
||||
>
|
||||
<main_modem_healthcheck />
|
||||
|
||||
|
@ -83,17 +74,6 @@ import { getFreedataMessages } from "../js/api";
|
|||
><i class="bi bi-rocket h3"></i
|
||||
></a>
|
||||
|
||||
<a
|
||||
class="list-group-item list-group-item-dark list-group-item-action border border-0 rounded-3 mb-2"
|
||||
id="list-info-list"
|
||||
data-bs-toggle="list"
|
||||
href="#list-info"
|
||||
role="tab"
|
||||
aria-controls="list-info"
|
||||
title="About"
|
||||
><i class="bi bi-info h3"></i
|
||||
></a>
|
||||
|
||||
<a
|
||||
class="list-group-item list-group-item-dark list-group-item-action d-none border-0 rounded-3 mb-2"
|
||||
id="list-logger-list"
|
||||
|
@ -111,6 +91,7 @@ import { getFreedataMessages } from "../js/api";
|
|||
role="tab"
|
||||
aria-controls="list-settings"
|
||||
title="Settings"
|
||||
@click="loadAllData"
|
||||
><i class="bi bi-gear-wide-connected h3"></i
|
||||
></a>
|
||||
</div>
|
||||
|
@ -143,27 +124,6 @@ import { getFreedataMessages } from "../js/api";
|
|||
<!-------------------------------- MAIN AREA ---------------->
|
||||
|
||||
<!------------------------------------------------------------------------------------------>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-5">
|
||||
<main_active_rig_control />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<main_active_broadcasts />
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<main_active_audio_level />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-7">
|
||||
<main_active_heard_stations />
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<main_active_stats />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -302,14 +262,7 @@ import { getFreedataMessages } from "../js/api";
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tab-pane fade"
|
||||
id="list-info"
|
||||
role="tabpanel"
|
||||
aria-labelledby="list-info-list"
|
||||
>
|
||||
<infoScreen />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="tab-pane fade show active"
|
||||
id="list-grid"
|
||||
|
|
|
@ -1,170 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { setActivePinia } from "pinia";
|
||||
import pinia from "../store/index";
|
||||
setActivePinia(pinia);
|
||||
|
||||
import { useStateStore } from "../store/stateStore.js";
|
||||
const state = useStateStore(pinia);
|
||||
</script>
|
||||
<template>
|
||||
<div class="card mb-1">
|
||||
<div class="card-header p-1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-1">
|
||||
<i class="bi bi-volume-up" style="font-size: 1.2rem"></i>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<strong>Audio</strong>
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<button
|
||||
type="button"
|
||||
id="audioModalButton"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#audioModal"
|
||||
class="btn btn-sm btn-outline-secondary me-1"
|
||||
>
|
||||
Tune
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-1 text-end">
|
||||
<button
|
||||
type="button"
|
||||
id="openHelpModalAudioLevel"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#audioLevelHelpModal"
|
||||
class="btn m-0 p-0 border-0"
|
||||
>
|
||||
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div
|
||||
class="progress mb-0 rounded-0 rounded-top"
|
||||
style="height: 22px"
|
||||
>
|
||||
<div
|
||||
class="progress-bar progress-bar-striped bg-primary force-gpu"
|
||||
id="noise_level"
|
||||
role="progressbar"
|
||||
:style="{ width: state.s_meter_strength_percent + '%' }"
|
||||
aria-valuenow="{{state.s_meter_strength_percent}}"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
></div>
|
||||
<p
|
||||
class="justify-content-center d-flex position-absolute w-100"
|
||||
id="noise_level_value"
|
||||
>
|
||||
S-Meter(dB): {{ state.s_meter_strength_raw }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="progress mb-0 rounded-0 rounded-bottom"
|
||||
style="height: 8px"
|
||||
>
|
||||
<div
|
||||
class="progress-bar progress-bar-striped bg-warning"
|
||||
role="progressbar"
|
||||
style="width: 1%"
|
||||
aria-valuenow="1"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
></div>
|
||||
<div
|
||||
class="progress-bar bg-success"
|
||||
role="progressbar"
|
||||
style="width: 89%"
|
||||
aria-valuenow="50"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
></div>
|
||||
<div
|
||||
class="progress-bar progress-bar-striped bg-warning"
|
||||
role="progressbar"
|
||||
style="width: 20%"
|
||||
aria-valuenow="20"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
></div>
|
||||
<div
|
||||
class="progress-bar progress-bar-striped bg-danger"
|
||||
role="progressbar"
|
||||
style="width: 29%"
|
||||
aria-valuenow="29"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div
|
||||
class="progress mb-0 rounded-0 rounded-top"
|
||||
style="height: 22px"
|
||||
>
|
||||
<div
|
||||
class="progress-bar progress-bar-striped bg-primary force-gpu"
|
||||
id="dbfs_level"
|
||||
role="progressbar"
|
||||
:style="{ width: state.dbfs_level_percent + '%' }"
|
||||
aria-valuenow="0"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
></div>
|
||||
<p
|
||||
class="justify-content-center d-flex position-absolute w-100"
|
||||
id="dbfs_level_value"
|
||||
>
|
||||
{{ state.dbfs_level }} dBFS
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="progress mb-0 rounded-0 rounded-bottom"
|
||||
style="height: 8px"
|
||||
>
|
||||
<div
|
||||
class="progress-bar progress-bar-striped bg-warning"
|
||||
role="progressbar"
|
||||
style="width: 1%"
|
||||
aria-valuenow="1"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
></div>
|
||||
<div
|
||||
class="progress-bar bg-success"
|
||||
role="progressbar"
|
||||
style="width: 89%"
|
||||
aria-valuenow="50"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
></div>
|
||||
<div
|
||||
class="progress-bar progress-bar-striped bg-warning"
|
||||
role="progressbar"
|
||||
style="width: 20%"
|
||||
aria-valuenow="20"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
></div>
|
||||
<div
|
||||
class="progress-bar progress-bar-striped bg-danger"
|
||||
role="progressbar"
|
||||
style="width: 29%"
|
||||
aria-valuenow="29"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,110 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
import { setActivePinia } from "pinia";
|
||||
import pinia from "../store/index";
|
||||
setActivePinia(pinia);
|
||||
|
||||
import { settingsStore as settings} from "../store/settingsStore.js";
|
||||
|
||||
import { useStateStore } from "../store/stateStore.js";
|
||||
const state = useStateStore(pinia);
|
||||
|
||||
import { sendModemCQ, sendModemPing, setModemBeacon } from "../js/api.js";
|
||||
|
||||
function transmitPing() {
|
||||
sendModemPing((<HTMLInputElement>document.getElementById("dxCall")).value);
|
||||
}
|
||||
|
||||
function startStopBeacon() {
|
||||
if (state.beacon_state === true) {
|
||||
setModemBeacon(false);
|
||||
}
|
||||
else {
|
||||
setModemBeacon(true);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="card mb-1">
|
||||
<div class="card-header p-1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-1">
|
||||
<i class="bi bi-broadcast" style="font-size: 1.2rem"></i>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<strong class="fs-5">Broadcasts</strong>
|
||||
</div>
|
||||
<div class="col-1 text-end">
|
||||
<button
|
||||
type="button"
|
||||
id="openHelpModalBroadcasts"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#broadcastsHelpModal"
|
||||
class="btn m-0 p-0 border-0"
|
||||
>
|
||||
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
<div class="row">
|
||||
<div class="col-md-auto">
|
||||
<div class="input-group input-group-sm mb-0">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
style="max-width: 6rem; text-transform: uppercase"
|
||||
placeholder="DXcall"
|
||||
pattern="[A-Z]*"
|
||||
id="dxCall"
|
||||
maxlength="11"
|
||||
aria-label="Input group"
|
||||
aria-describedby="btnGroupAddon"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-sm btn-outline-secondary ms-1"
|
||||
id="sendPing"
|
||||
type="button"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-trigger="hover"
|
||||
data-bs-html="false"
|
||||
title="Send a ping request to a remote station"
|
||||
@click="transmitPing()"
|
||||
>
|
||||
Ping
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-sm btn-outline-secondary ms-1"
|
||||
id="sendCQ"
|
||||
type="button"
|
||||
title="Send a CQ to the world"
|
||||
@click="sendModemCQ()"
|
||||
>
|
||||
Call CQ
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
id="startBeacon"
|
||||
class="btn btn-sm ms-1"
|
||||
@click="startStopBeacon()"
|
||||
v-bind:class="{
|
||||
'btn-success': state.beacon_state === true,
|
||||
'btn-outline-secondary': state.beacon_state === false,
|
||||
}"
|
||||
title="Toggle beacon mode. The interval can be set in settings. While sending a beacon, you can receive ping requests and open a datachannel. If a datachannel is opened, the beacon pauses."
|
||||
>
|
||||
<i class="bi bi-soundwave"></i> Toggle beacon
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end of row-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,122 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
// @ts-nocheck
|
||||
const { distance } = require("qth-locator");
|
||||
|
||||
import { setActivePinia } from "pinia";
|
||||
import pinia from "../store/index";
|
||||
setActivePinia(pinia);
|
||||
|
||||
import { settingsStore as settings } from "../store/settingsStore.js";
|
||||
|
||||
import { useStateStore } from "../store/stateStore.js";
|
||||
const state = useStateStore(pinia);
|
||||
|
||||
function getDateTime(timestampRaw) {
|
||||
var datetime = new Date(timestampRaw * 1000).toLocaleString(
|
||||
navigator.language,
|
||||
{
|
||||
hourCycle: "h23",
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
},
|
||||
);
|
||||
return datetime;
|
||||
}
|
||||
|
||||
function getMaidenheadDistance(dxGrid) {
|
||||
if (typeof dxGrid != "undefined") {
|
||||
try {
|
||||
return parseInt(distance(settings.remote.STATION.mygrid, dxGrid));
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="card mb-1 h-100">
|
||||
<!--325px-->
|
||||
<div class="card-header p-1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<i class="bi bi-list-columns-reverse" style="font-size: 1.2rem"></i>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<strong class="fs-5">Heard stations</strong>
|
||||
</div>
|
||||
<div class="col-1 text-end">
|
||||
<button
|
||||
type="button"
|
||||
id="openHelpModalHeardStations"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#heardStationsHelpModal"
|
||||
class="btn m-0 p-0 border-0"
|
||||
>
|
||||
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0" style="overflow-y: overlay">
|
||||
<div class="table-responsive">
|
||||
<!-- START OF TABLE FOR HEARD STATIONS -->
|
||||
<table class="table table-sm" id="tblHeardStationList">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" id="thTime">
|
||||
<i id="hslSort" class="bi bi-sort-up"></i>Time
|
||||
</th>
|
||||
<th scope="col" id="thFreq">Freq</th>
|
||||
<th scope="col" id="thDxcall">DXCall</th>
|
||||
<th scope="col" id="thDxgrid">Grid</th>
|
||||
<th scope="col" id="thDist">Dist</th>
|
||||
<th scope="col" id="thType">Type</th>
|
||||
<th scope="col" id="thSnr">SNR</th>
|
||||
<!--<th scope="col">Off</th>-->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="heardstations">
|
||||
<!--https://vuejs.org/guide/essentials/list.html-->
|
||||
<tr v-for="item in state.heard_stations" :key="item.origin">
|
||||
<td>
|
||||
<span class="badge bg-secondary">{{
|
||||
getDateTime(item.timestamp)
|
||||
}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary"
|
||||
>{{ item.frequency / 1000 }} kHz</span
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary">{{ item.origin }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary">{{ item.gridsquare }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary"
|
||||
>{{ getMaidenheadDistance(item.gridsquare) }} km</span
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary">{{ item.activity_type }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary">{{ item.snr }}</span>
|
||||
</td>
|
||||
<!--<td>{{ item.offset }}</td>-->
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- END OF HEARD STATIONS TABLE -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,261 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { setActivePinia } from "pinia";
|
||||
import pinia from "../store/index";
|
||||
setActivePinia(pinia);
|
||||
|
||||
import { useStateStore } from "../store/stateStore.js";
|
||||
const state = useStateStore(pinia);
|
||||
|
||||
import { setRadioParametersFrequency, setRadioParametersMode, setRadioParametersRFLevel } from "../js/api";
|
||||
|
||||
function updateFrequencyAndApply(frequency) {
|
||||
state.new_frequency = frequency;
|
||||
set_radio_parameter_frequency();
|
||||
}
|
||||
|
||||
function set_radio_parameter_frequency(){
|
||||
setRadioParametersFrequency(state.new_frequency)
|
||||
}
|
||||
|
||||
function set_radio_parameter_mode(){
|
||||
setRadioParametersMode(state.mode)
|
||||
}
|
||||
|
||||
function set_radio_parameter_rflevel(){
|
||||
setRadioParametersRFLevel(state.rf_level)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<div class="card mb-1">
|
||||
<div class="card-header p-1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-1">
|
||||
<i class="bi bi-house-door" style="font-size: 1.2rem"></i>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<strong class="fs-5 me-2">Radio control</strong>
|
||||
<span
|
||||
class="badge"
|
||||
v-bind:class="{
|
||||
'text-bg-success': state.hamlib_status === 'connected',
|
||||
'text-bg-danger disabled':
|
||||
state.hamlib_status === 'disconnected',
|
||||
}"
|
||||
>{{ state.hamlib_status }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="col-1 text-end">
|
||||
<button
|
||||
type="button"
|
||||
id="openHelpModalStation"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#stationHelpModal"
|
||||
class="btn m-0 p-0 border-0"
|
||||
disabled
|
||||
>
|
||||
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
<div class="input-group input-group-sm bottom-0 m-0">
|
||||
<div class="me-2">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text">QRG</span>
|
||||
<span class="input-group-text">{{ state.frequency }} Hz</span>
|
||||
|
||||
<!-- Dropdown Button -->
|
||||
<button
|
||||
v-bind:class="{
|
||||
disabled: state.hamlib_status === 'disconnected',
|
||||
}"
|
||||
class="btn btn-secondary dropdown-toggle"
|
||||
type="button"
|
||||
id="dropdownMenuButton"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
></button>
|
||||
|
||||
<!-- Dropdown Menu -->
|
||||
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton">
|
||||
<li>
|
||||
<div class="input-group p-1">
|
||||
<span class="input-group-text">frequency</span>
|
||||
|
||||
<input
|
||||
v-model="state.new_frequency"
|
||||
style="max-width: 8rem"
|
||||
pattern="[0-9]*"
|
||||
type="text"
|
||||
class="form-control form-control-sm"
|
||||
v-bind:class="{
|
||||
disabled: state.hamlib_status === 'disconnected',
|
||||
}"
|
||||
placeholder="Type frequency..."
|
||||
aria-label="Frequency"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-sm btn-outline-success"
|
||||
type="button"
|
||||
@click="updateFrequencyAndApply(state.new_frequency)"
|
||||
v-bind:class="{
|
||||
disabled: state.hamlib_status === 'disconnected',
|
||||
}"
|
||||
>
|
||||
<i class="bi bi-check-square"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Dropdown Divider -->
|
||||
<li><hr class="dropdown-divider" /></li>
|
||||
<!-- Dropdown Items -->
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
@click="updateFrequencyAndApply(50616000)"
|
||||
><strong>50616 kHz</strong>
|
||||
<span class="badge bg-secondary">6m | USB</span>
|
||||
<span class="badge bg-info">EU | US</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
@click="updateFrequencyAndApply(50308000)"
|
||||
><strong>50308 kHz</strong>
|
||||
<span class="badge bg-secondary">6m | USB</span>
|
||||
<span class="badge bg-info">US</span></a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
@click="updateFrequencyAndApply(28093000)"
|
||||
><strong>28093 kHz</strong>
|
||||
<span class="badge bg-secondary">10m | USB</span>
|
||||
<span class="badge bg-info">EU | US</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
@click="updateFrequencyAndApply(27265000)"
|
||||
><strong>27265 kHz</strong>
|
||||
<span class="badge bg-secondary">11m | USB</span>
|
||||
<span class="badge bg-dark">Ch 26</span></a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
@click="updateFrequencyAndApply(27245000)"
|
||||
><strong>27245 kHz</strong>
|
||||
<span class="badge bg-secondary">11m | USB</span>
|
||||
<span class="badge bg-dark">Ch 25</span></a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
@click="updateFrequencyAndApply(24908000)"
|
||||
><strong>24908 kHz</strong>
|
||||
<span class="badge bg-secondary">12m | USB</span>
|
||||
<span class="badge bg-info">EU | US</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
@click="updateFrequencyAndApply(21093000)"
|
||||
><strong>21093 kHz</strong>
|
||||
<span class="badge bg-secondary">15m | USB</span>
|
||||
<span class="badge bg-info">EU | US</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
@click="updateFrequencyAndApply(14093000)"
|
||||
><strong>14093 kHz</strong>
|
||||
<span class="badge bg-secondary">20m | USB</span>
|
||||
<span class="badge bg-info">EU | US</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
href="#"
|
||||
@click="updateFrequencyAndApply(7053000)"
|
||||
><strong>7053 kHz</strong>
|
||||
<span class="badge bg-secondary">40m | USB</span>
|
||||
<span class="badge bg-info">EU | US</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="me-2">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text">Mode</span>
|
||||
<select
|
||||
class="form-control"
|
||||
v-model="state.mode"
|
||||
@click="set_radio_parameter_mode()"
|
||||
v-bind:class="{
|
||||
disabled: state.hamlib_status === 'disconnected',
|
||||
}"
|
||||
>
|
||||
<option value="USB">USB</option>
|
||||
<option value="USB-D">USB-D</option>
|
||||
<option value="PKTUSB">PKT-U</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="me-2">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text">Power</span>
|
||||
<select
|
||||
class="form-control"
|
||||
v-model="state.rf_level"
|
||||
@click="set_radio_parameter_rflevel()"
|
||||
v-bind:class="{
|
||||
disabled: state.hamlib_status === 'disconnected',
|
||||
}"
|
||||
>
|
||||
<option value="0">-</option>
|
||||
<option value="10">10</option>
|
||||
<option value="20">20</option>
|
||||
<option value="30">30</option>
|
||||
<option value="40">40</option>
|
||||
<option value="50">50</option>
|
||||
<option value="60">60</option>
|
||||
<option value="70">70</option>
|
||||
<option value="80">80</option>
|
||||
<option value="90">90</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,402 +0,0 @@
|
|||
<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 { setActivePinia } from "pinia";
|
||||
import pinia from "../store/index";
|
||||
setActivePinia(pinia);
|
||||
|
||||
import { settingsStore as settings } from "../store/settingsStore.js";
|
||||
|
||||
import { useStateStore } from "../store/stateStore.js";
|
||||
const state = useStateStore(pinia);
|
||||
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
} from "chart.js";
|
||||
import { Line, Scatter } from "vue-chartjs";
|
||||
import { computed } from "vue";
|
||||
|
||||
function selectStatsControl(obj) {
|
||||
switch (obj.delegateTarget.id) {
|
||||
case "list-waterfall-list":
|
||||
settings.local.spectrum = "waterfall";
|
||||
break;
|
||||
case "list-scatter-list":
|
||||
settings.local.spectrum = "scatter";
|
||||
break;
|
||||
case "list-chart-list":
|
||||
settings.local.spectrum = "chart";
|
||||
break;
|
||||
default:
|
||||
settings.local.spectrum = "waterfall";
|
||||
}
|
||||
//saveSettingsToFile();
|
||||
}
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
);
|
||||
|
||||
// https://www.chartjs.org/docs/latest/samples/line/segments.html
|
||||
const skipped = (speedCtx, value) =>
|
||||
speedCtx.p0.skip || speedCtx.p1.skip ? value : undefined;
|
||||
const down = (speedCtx, value) =>
|
||||
speedCtx.p0.parsed.y > speedCtx.p1.parsed.y ? value : undefined;
|
||||
|
||||
var transmissionSpeedChartOptions = {
|
||||
//type: "line",
|
||||
responsive: true,
|
||||
animations: true,
|
||||
maintainAspectRatio: false,
|
||||
cubicInterpolationMode: "monotone",
|
||||
tension: 0.4,
|
||||
scales: {
|
||||
SNR: {
|
||||
type: "linear",
|
||||
ticks: { beginAtZero: false, color: "rgb(255, 99, 132)" },
|
||||
position: "right",
|
||||
},
|
||||
SPEED: {
|
||||
type: "linear",
|
||||
ticks: { beginAtZero: false, color: "rgb(120, 100, 120)" },
|
||||
position: "left",
|
||||
grid: {
|
||||
drawOnChartArea: false, // only want the grid lines for one axis to show up
|
||||
},
|
||||
},
|
||||
x: { ticks: { beginAtZero: true } },
|
||||
},
|
||||
};
|
||||
|
||||
const transmissionSpeedChartData = computed(() => ({
|
||||
labels: state.arq_speed_list_timestamp,
|
||||
datasets: [
|
||||
{
|
||||
type: "line",
|
||||
label: "SNR[dB]",
|
||||
data: state.arq_speed_list_snr,
|
||||
borderColor: "rgb(75, 192, 192, 1.0)",
|
||||
pointRadius: 1,
|
||||
segment: {
|
||||
borderColor: (speedCtx) =>
|
||||
skipped(speedCtx, "rgb(0,0,0,0.4)") ||
|
||||
down(speedCtx, "rgb(192,75,75)"),
|
||||
borderDash: (speedCtx) => skipped(speedCtx, [3, 3]),
|
||||
},
|
||||
spanGaps: true,
|
||||
backgroundColor: "rgba(75, 192, 192, 0.2)",
|
||||
order: 1,
|
||||
yAxisID: "SNR",
|
||||
},
|
||||
{
|
||||
type: "bar",
|
||||
label: "Speed[bpm]",
|
||||
data: state.arq_speed_list_bpm,
|
||||
borderColor: "rgb(120, 100, 120, 1.0)",
|
||||
backgroundColor: "rgba(120, 100, 120, 0.2)",
|
||||
order: 0,
|
||||
yAxisID: "SPEED",
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
const scatterChartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
type: "linear",
|
||||
position: "bottom",
|
||||
grid: {
|
||||
display: true,
|
||||
lineWidth: 1, // Set the line width for x-axis grid lines
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
type: "linear",
|
||||
position: "left",
|
||||
grid: {
|
||||
display: true,
|
||||
lineWidth: 1, // Set the line width for y-axis grid lines
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// dummy data
|
||||
//state.scatter = [{"x":"166","y":"46"},{"x":"-193","y":"-139"},{"x":"-165","y":"-291"},{"x":"311","y":"-367"},{"x":"389","y":"199"},{"x":"78","y":"372"},{"x":"242","y":"-431"},{"x":"-271","y":"-248"},{"x":"28","y":"-130"},{"x":"-20","y":"187"},{"x":"74","y":"362"},{"x":"-316","y":"-229"},{"x":"-180","y":"261"},{"x":"321","y":"360"},{"x":"438","y":"-288"},{"x":"378","y":"-94"},{"x":"462","y":"-163"},{"x":"-265","y":"248"},{"x":"210","y":"314"},{"x":"230","y":"-320"},{"x":"261","y":"-244"},{"x":"-283","y":"-373"}]
|
||||
|
||||
const scatterChartData = computed(() => ({
|
||||
datasets: [
|
||||
{
|
||||
type: "scatter",
|
||||
fill: true,
|
||||
data: state.scatter,
|
||||
label: "Scatter",
|
||||
tension: 0.1,
|
||||
borderColor: "rgb(0, 255, 0)",
|
||||
},
|
||||
],
|
||||
}));
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { initWaterfall, setColormap } from "../js/waterfallHandler.js";
|
||||
var localSpectrum;
|
||||
export default {
|
||||
mounted() {
|
||||
// This code will be executed after the component is mounted to the DOM
|
||||
// You can access DOM elements or perform other initialization here
|
||||
//const myElement = this.$refs.waterfall; // Access the DOM element with ref
|
||||
|
||||
// init waterfall
|
||||
localSpectrum = initWaterfall("waterfall-main");
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card mb-1" style="height: calc(var(--variable-height) - 20px)">
|
||||
<div class="card-header p-1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-11 p-0">
|
||||
<div class="btn-group h-100" role="group">
|
||||
<div
|
||||
class="list-group bg-body-tertiary list-group-horizontal"
|
||||
id="list-tab"
|
||||
role="tablist"
|
||||
>
|
||||
<a
|
||||
class="py-0 list-group-item list-group-item-dark list-group-item-action"
|
||||
id="list-waterfall-list"
|
||||
data-bs-toggle="list"
|
||||
href="#list-waterfall"
|
||||
role="tab"
|
||||
aria-controls="list-waterfall"
|
||||
v-bind:class="{
|
||||
active: settings.local.spectrum === 'waterfall',
|
||||
}"
|
||||
@click="selectStatsControl($event)"
|
||||
><strong><i class="bi bi-water"></i></strong
|
||||
></a>
|
||||
<a
|
||||
class="py-0 list-group-item list-group-item-dark list-group-item-action"
|
||||
id="list-scatter-list"
|
||||
data-bs-toggle="list"
|
||||
href="#list-scatter"
|
||||
role="tab"
|
||||
aria-controls="list-scatter"
|
||||
v-bind:class="{
|
||||
active: settings.local.spectrum === 'scatter',
|
||||
}"
|
||||
@click="selectStatsControl($event)"
|
||||
><strong><i class="bi bi-border-outer"></i></strong
|
||||
></a>
|
||||
<a
|
||||
class="py-0 list-group-item list-group-item-dark list-group-item-action"
|
||||
id="list-chart-list"
|
||||
data-bs-toggle="list"
|
||||
href="#list-chart"
|
||||
role="tab"
|
||||
aria-controls="list-chart"
|
||||
v-bind:class="{ active: settings.local.spectrum === 'chart' }"
|
||||
@click="selectStatsControl($event)"
|
||||
><strong><i class="bi bi-graph-up-arrow"></i></strong
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group" role="group" aria-label="Busy indicators">
|
||||
<button
|
||||
class="btn btn-sm ms-1 p-1 disabled"
|
||||
type="button"
|
||||
data-bs-placement="top"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-trigger="hover"
|
||||
data-bs-html="true"
|
||||
v-bind:class="{
|
||||
'btn-warning': state.channel_busy_slot[0] === true,
|
||||
'btn-outline-secondary': state.channel_busy_slot[0] === false,
|
||||
}"
|
||||
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
|
||||
>
|
||||
S1
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-sm p-1 disabled"
|
||||
type="button"
|
||||
data-bs-placement="top"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-trigger="hover"
|
||||
data-bs-html="true"
|
||||
v-bind:class="{
|
||||
'btn-warning': state.channel_busy_slot[1] === true,
|
||||
'btn-outline-secondary': state.channel_busy_slot[1] === false,
|
||||
}"
|
||||
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
|
||||
>
|
||||
S2
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-sm p-1 disabled"
|
||||
type="button"
|
||||
data-bs-placement="top"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-trigger="hover"
|
||||
data-bs-html="true"
|
||||
v-bind:class="{
|
||||
'btn-warning': state.channel_busy_slot[2] === true,
|
||||
'btn-outline-secondary': state.channel_busy_slot[2] === false,
|
||||
}"
|
||||
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
|
||||
>
|
||||
S3
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-sm p-1 disabled"
|
||||
type="button"
|
||||
data-bs-placement="top"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-trigger="hover"
|
||||
data-bs-html="true"
|
||||
v-bind:class="{
|
||||
'btn-warning': state.channel_busy_slot[3] === true,
|
||||
'btn-outline-secondary': state.channel_busy_slot[3] === false,
|
||||
}"
|
||||
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
|
||||
>
|
||||
S4
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-sm p-1 disabled"
|
||||
type="button"
|
||||
data-bs-placement="top"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-trigger="hover"
|
||||
data-bs-html="true"
|
||||
v-bind:class="{
|
||||
'btn-warning': state.channel_busy_slot[4] === true,
|
||||
'btn-outline-secondary': state.channel_busy_slot[4] === false,
|
||||
}"
|
||||
title="Channel busy state: <strong class='text-success'>not busy</strong> / <strong class='text-danger'>busy </strong>"
|
||||
>
|
||||
S5
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-sm p-1 disabled"
|
||||
type="button"
|
||||
data-bs-placement="top"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-trigger="hover"
|
||||
data-bs-html="true"
|
||||
title="Recieving data: illuminates <strong class='text-success'>green</strong> if receiving codec2 data"
|
||||
v-bind:class="{
|
||||
'btn-success': state.is_codec2_traffic === true,
|
||||
'btn-outline-secondary': state.is_codec2_traffic === false,
|
||||
}"
|
||||
>
|
||||
data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-1 text-end">
|
||||
<button
|
||||
type="button"
|
||||
id="openHelpModalWaterfall"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#waterfallHelpModal"
|
||||
class="btn m-0 p-0 border-0"
|
||||
>
|
||||
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-1">
|
||||
<div class="tab-content" id="nav-stats-tabContent">
|
||||
<div
|
||||
class="tab-pane fade"
|
||||
v-bind:class="{
|
||||
'show active': settings.local.spectrum === 'waterfall',
|
||||
}"
|
||||
id="list-waterfall"
|
||||
role="stats_tabpanel"
|
||||
aria-labelledby="list-waterfall-list"
|
||||
>
|
||||
<canvas
|
||||
ref="waterfall-main"
|
||||
id="waterfall-main"
|
||||
style="
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
aspect-ratio: unset;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
"
|
||||
class="force-gpu'"
|
||||
></canvas>
|
||||
</div>
|
||||
<div
|
||||
class="tab-pane fade"
|
||||
v-bind:class="{
|
||||
'show active': settings.local.spectrum === 'scatter',
|
||||
}"
|
||||
id="list-scatter"
|
||||
role="tabpanel"
|
||||
aria-labelledby="list-scatter-list"
|
||||
>
|
||||
<Scatter :data="scatterChartData" :options="scatterChartOptions" />
|
||||
</div>
|
||||
<div
|
||||
class="tab-pane fade"
|
||||
v-bind:class="{ 'show active': settings.local.spectrum === 'chart' }"
|
||||
id="list-chart"
|
||||
role="tabpanel"
|
||||
aria-labelledby="list-chart-list"
|
||||
>
|
||||
<Line
|
||||
:data="transmissionSpeedChartData"
|
||||
:options="transmissionSpeedChartOptions"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--278px-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,62 +0,0 @@
|
|||
<script setup>
|
||||
import { setActivePinia } from "pinia";
|
||||
import pinia from "../store/index";
|
||||
setActivePinia(pinia);
|
||||
|
||||
import { useAudioStore } from "../store/audioStore.js";
|
||||
const audio = useAudioStore(pinia);
|
||||
|
||||
import { setConfig } from "../js/api";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card mb-0">
|
||||
<div class="card-header p-1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-1">
|
||||
<i class="bi bi-volume-up" style="font-size: 1.2rem"></i>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<strong class="fs-5">Audio devices</strong>
|
||||
</div>
|
||||
<div class="col-1 text-end">
|
||||
<button
|
||||
type="button"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#audioHelpModal"
|
||||
class="btn m-0 p-0 border-0"
|
||||
>
|
||||
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body p-2" style="height: 100px">
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<span class="input-group-text">
|
||||
<i class="bi bi-mic-fill" style="font-size: 1rem"></i>
|
||||
</span>
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
aria-label=".form-select-sm"
|
||||
v-html="audio.getInputDevices()"
|
||||
@change="setConfig"
|
||||
></select>
|
||||
</div>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text">
|
||||
<i class="bi bi-volume-up" style="font-size: 1rem"></i>
|
||||
</span>
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
aria-label=".form-select-sm"
|
||||
v-html="audio.getOutputDevices()"
|
||||
@change="setConfig"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,114 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { setActivePinia } from "pinia";
|
||||
import pinia from "../store/index";
|
||||
setActivePinia(pinia);
|
||||
|
||||
import { settingsStore as settings } from "../store/settingsStore.js";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card mb-1">
|
||||
<div class="card-header p-1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-1">
|
||||
<i class="bi bi-house-door" style="font-size: 1.2rem"></i>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<strong class="fs-5">My station</strong>
|
||||
</div>
|
||||
<div class="col-1 text-end">
|
||||
<button
|
||||
type="button"
|
||||
id="openHelpModalStation"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#stationHelpModal"
|
||||
class="btn m-0 p-0 border-0"
|
||||
>
|
||||
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
<div class="row">
|
||||
<div class="col-md-auto">
|
||||
<div
|
||||
class="input-group input-group-sm mb-0"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-trigger="hover"
|
||||
data-bs-html="false"
|
||||
title="Enter your callsign and save it"
|
||||
>
|
||||
<span class="input-group-text">
|
||||
<i class="bi bi-person-bounding-box" style="font-size: 1rem"></i>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
style="width: 5rem; text-transform: uppercase"
|
||||
placeholder="callsign"
|
||||
pattern="[A-Z]*"
|
||||
id="myCall"
|
||||
maxlength="8"
|
||||
aria-label="Input group"
|
||||
aria-describedby="btnGroupAddon"
|
||||
v-model="settings.remote.STATION.mycall"
|
||||
/>
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
aria-label=".form-select-sm"
|
||||
id="myCallSSID"
|
||||
v-model="settings.remote.STATION.myssid"
|
||||
>
|
||||
<option selected value="0">0</option>
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
<option value="5">5</option>
|
||||
<option value="6">6</option>
|
||||
<option value="7">7</option>
|
||||
<option value="8">8</option>
|
||||
<option value="9">9</option>
|
||||
<option value="10">10</option>
|
||||
<option value="11">11</option>
|
||||
<option value="12">12</option>
|
||||
<option value="13">13</option>
|
||||
<option value="14">14</option>
|
||||
<option value="15">15</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-auto">
|
||||
<div
|
||||
class="input-group input-group-sm mb-0"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-trigger="hover"
|
||||
data-bs-html="false"
|
||||
title="Enter your gridsquare and save it"
|
||||
>
|
||||
<span class="input-group-text">
|
||||
<i class="bi bi-house-fill" style="font-size: 1rem"></i>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control mr-1"
|
||||
style="max-width: 6rem"
|
||||
placeholder="locator"
|
||||
id="myGrid"
|
||||
maxlength="6"
|
||||
aria-label="Input group"
|
||||
aria-describedby="btnGroupAddon"
|
||||
v-model="settings.remote.STATION.mygrid"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- end of row-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,235 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { setActivePinia } from "pinia";
|
||||
import pinia from "../store/index";
|
||||
setActivePinia(pinia);
|
||||
|
||||
import { settingsStore as settings } from "../store/settingsStore.js";
|
||||
|
||||
import { useStateStore } from "../store/stateStore.js";
|
||||
const state = useStateStore(pinia);
|
||||
|
||||
function selectRadioControl() {
|
||||
// @ts-expect-error
|
||||
switch (event.target.id) {
|
||||
case "list-rig-control-none-list":
|
||||
settings.remote.RADIO.control = "disabled";
|
||||
break;
|
||||
case "list-rig-control-rigctld-list":
|
||||
settings.remote.RADIO.control = "rigctld";
|
||||
break;
|
||||
case "list-rig-control-rigctld-list":
|
||||
settings.remote.RADIO.control = "rigctld_bundle";
|
||||
break;
|
||||
|
||||
case "list-rig-control-tci-list":
|
||||
settings.remote.RADIO.control = "tci";
|
||||
break;
|
||||
default:
|
||||
console.log("default=!==");
|
||||
settings.remote.RADIO.control = "disabled";
|
||||
}
|
||||
//saveSettingsToFile();
|
||||
}
|
||||
|
||||
function testHamlib() {
|
||||
console.log("not yet implemented");
|
||||
alert("not yet implemented");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-3">
|
||||
<div class="card mb-1">
|
||||
<div class="card-header p-1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-1">
|
||||
<i class="bi bi-projector" style="font-size: 1.2rem"></i>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<strong class="fs-5">Rig control</strong>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<div
|
||||
class="list-group bg-body-tertiary list-group-horizontal w-75"
|
||||
id="rig-control-list-tab"
|
||||
role="rig-control-tablist"
|
||||
>
|
||||
<a
|
||||
class="p-1 list-group-item list-group-item-dark list-group-item-action"
|
||||
id="list-rig-control-none-list"
|
||||
data-bs-toggle="list"
|
||||
href="#list-rig-control-none"
|
||||
role="tab"
|
||||
aria-controls="list-rig-control-none"
|
||||
v-bind:class="{
|
||||
active: settings.remote.RADIO.control === 'disabled',
|
||||
}"
|
||||
@click="selectRadioControl()"
|
||||
>None</a
|
||||
>
|
||||
<a
|
||||
class="p-1 list-group-item list-group-item-dark list-group-item-action"
|
||||
id="list-rig-control-rigctld-list"
|
||||
data-bs-toggle="list"
|
||||
href="#list-rig-control-rigctld"
|
||||
role="tab"
|
||||
aria-controls="list-rig-control-rigctld"
|
||||
v-bind:class="{
|
||||
active: settings.remote.RADIO.control === 'rigctld',
|
||||
}"
|
||||
@click="selectRadioControl()"
|
||||
>Rigctld</a
|
||||
>
|
||||
<a
|
||||
class="p-1 list-group-item list-group-item-dark list-group-item-action"
|
||||
id="list-rig-control-tci-list"
|
||||
data-bs-toggle="list"
|
||||
href="#list-rig-control-tci"
|
||||
role="tab"
|
||||
aria-controls="list-rig-control-tci"
|
||||
v-bind:class="{
|
||||
active: settings.remote.RADIO.control === 'tci',
|
||||
}"
|
||||
@click="selectRadioControl()"
|
||||
>TCI</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-1 text-end">
|
||||
<button
|
||||
type="button"
|
||||
id="openHelpModalRigControl"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#rigcontrolHelpModal"
|
||||
class="btn m-0 p-0 border-0"
|
||||
>
|
||||
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-2" style="height: 100px">
|
||||
<div class="tab-content" id="rig-control-nav-tabContent">
|
||||
<div
|
||||
class="tab-pane fade"
|
||||
v-bind:class="{
|
||||
'show active': settings.remote.RADIO.control === 'disabled',
|
||||
}"
|
||||
id="list-rig-control-none"
|
||||
role="tabpanel"
|
||||
aria-labelledby="list-rig-control-none-list"
|
||||
>
|
||||
<p class="small">
|
||||
Modem will not utilize rig control and features will be limited.
|
||||
While functional; it is recommended to configure hamlib. <br />
|
||||
Use this setting also for <strong> VOX </strong>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="tab-pane fade"
|
||||
id="list-rig-control-rigctld"
|
||||
v-bind:class="{
|
||||
'show active': settings.remote.RADIO.control === 'rigctld',
|
||||
}"
|
||||
role="tabpanel"
|
||||
aria-labelledby="list-rig-control-rigctld-list"
|
||||
>
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<span class="input-group-text">Rigctld service</span>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Status"
|
||||
id="hamlib_rigctld_status"
|
||||
aria-label="State"
|
||||
aria-describedby="basic-addon1"
|
||||
v-model="state.rigctld_started"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
id="testHamlib"
|
||||
class="btn btn-sm btn-outline-secondary ms-1"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-trigger="hover"
|
||||
data-bs-html="true"
|
||||
@click="testHamlib"
|
||||
title="Test your hamlib settings and toggle PTT once. Button will become <strong class='text-success'>green</strong> on success and <strong class='text-danger'>red</strong> if fails."
|
||||
>
|
||||
PTT Test
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tab-pane fade"
|
||||
id="list-rig-control-tci"
|
||||
v-bind:class="{
|
||||
'show active': settings.remote.RADIO.control === 'tci',
|
||||
}"
|
||||
role="tabpanel"
|
||||
aria-labelledby="list-rig-control-tci-list"
|
||||
>
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<span class="input-group-text">TCI</span>
|
||||
<span class="input-group-text">Address</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="tci IP"
|
||||
id="tci_ip"
|
||||
aria-label="Device IP"
|
||||
v-model="settings.remote.TCI.tci_ip"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<span class="input-group-text">Port</span>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="tci port"
|
||||
id="tci_port"
|
||||
aria-label="Device Port"
|
||||
v-model="settings.remote.TCI.tci_port"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RADIO CONTROL DISABLED -->
|
||||
<div id="radio-control-disabled"></div>
|
||||
|
||||
<!-- RADIO CONTROL RIGCTLD -->
|
||||
<div id="radio-control-rigctld"></div>
|
||||
<!-- RADIO CONTROL TCI-->
|
||||
<div id="radio-control-tci"></div>
|
||||
<!-- RADIO CONTROL HELP -->
|
||||
<div id="radio-control-help">
|
||||
<!--
|
||||
<strong>VOX:</strong> Use rig control mode 'none'
|
||||
<br />
|
||||
<strong>HAMLIB locally:</strong> configure in settings, then
|
||||
start/stop service.
|
||||
<br />
|
||||
<strong>HAMLIB remotely:</strong> Enter IP/Port, connection
|
||||
happens automatically.
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
<!--<div class="card-footer text-muted small" id="hamlib_info_field">
|
||||
Define Modem rig control mode (none/hamlib)
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -2,8 +2,6 @@
|
|||
import { Modal } from "bootstrap";
|
||||
import { onMounted } from "vue";
|
||||
|
||||
import settings_updater_core from "./settings_updater_core.vue";
|
||||
|
||||
import { setActivePinia } from "pinia";
|
||||
import pinia from "../store/index";
|
||||
setActivePinia(pinia);
|
||||
|
@ -14,6 +12,11 @@ import { sendModemCQ } from "../js/api.js";
|
|||
import { useStateStore } from "../store/stateStore.js";
|
||||
const state = useStateStore(pinia);
|
||||
|
||||
import { useAudioStore } from "../store/audioStore";
|
||||
const audioStore = useAudioStore();
|
||||
import { useSerialStore } from "../store/serialStore";
|
||||
const serialStore = useSerialStore();
|
||||
|
||||
import {
|
||||
getVersion,
|
||||
setConfig,
|
||||
|
@ -21,11 +24,8 @@ import {
|
|||
stopModem,
|
||||
getModemState,
|
||||
} from "../js/api";
|
||||
import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
|
||||
import { serialDeviceOptions } from "../js/deviceFormHelper";
|
||||
|
||||
const version = import.meta.env.PACKAGE_VERSION;
|
||||
var updateAvailable = process.env.FDUpdateAvail;
|
||||
|
||||
// start modemCheck modal once on startup
|
||||
onMounted(() => {
|
||||
|
@ -126,6 +126,7 @@ function testHamlib() {
|
|||
max="65534"
|
||||
min="1025"
|
||||
v-model="settings.local.port"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -137,6 +138,7 @@ function testHamlib() {
|
|||
placeholder="modem host (default 127.0.0.1)"
|
||||
id="modem_port"
|
||||
v-model="settings.local.host"
|
||||
@change="onChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -203,6 +205,7 @@ function testHamlib() {
|
|||
</button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Audio Input Device -->
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<label class="input-group-text w-50"
|
||||
|
@ -215,10 +218,10 @@ function testHamlib() {
|
|||
v-model="settings.remote.AUDIO.input_device"
|
||||
>
|
||||
<option
|
||||
v-for="option in audioInputOptions()"
|
||||
v-bind:value="option.id"
|
||||
v-for="device in audioStore.audioInputs"
|
||||
:value="device.id"
|
||||
>
|
||||
{{ option.name }} [{{ option.api }}]
|
||||
{{ device.name }} [{{ device.api }}]
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
@ -235,10 +238,10 @@ function testHamlib() {
|
|||
v-model="settings.remote.AUDIO.output_device"
|
||||
>
|
||||
<option
|
||||
v-for="option in audioOutputOptions()"
|
||||
v-bind:value="option.id"
|
||||
v-for="device in audioStore.audioOutputs"
|
||||
:value="device.id"
|
||||
>
|
||||
{{ option.name }} [{{ option.api }}]
|
||||
{{ device.name }} [{{ device.api }}]
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
@ -310,18 +313,16 @@ function testHamlib() {
|
|||
>
|
||||
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
aria-label=".form-select-sm"
|
||||
id="hamlib_deviceport"
|
||||
style="width: 7rem"
|
||||
@change="onChange"
|
||||
v-model="settings.remote.RADIO.serial_port"
|
||||
class="form-select form-select-sm"
|
||||
>
|
||||
<option
|
||||
v-for="option in serialDeviceOptions()"
|
||||
v-bind:value="option.port"
|
||||
v-for="device in serialStore.serialDevices"
|
||||
:value="device.port"
|
||||
:key="device.port"
|
||||
>
|
||||
{{ option.description }}
|
||||
{{ device.description }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
@ -392,18 +393,6 @@ function testHamlib() {
|
|||
data-bs-toggle="collapse"
|
||||
>
|
||||
Version
|
||||
<span
|
||||
class="badge ms-2"
|
||||
:class="
|
||||
updateAvailable === '1' ? 'bg-warning' : 'bg-success'
|
||||
"
|
||||
>
|
||||
{{
|
||||
updateAvailable === "1"
|
||||
? "Update available ! ! ! !"
|
||||
: "Current"
|
||||
}}</span
|
||||
>
|
||||
</button>
|
||||
</h2>
|
||||
<div
|
||||
|
@ -426,9 +415,6 @@ function testHamlib() {
|
|||
>
|
||||
Modem version | {{ state.modem_version }}
|
||||
</button>
|
||||
<div :class="updateAvailable === '1' ? '' : 'd-none'">
|
||||
<settings_updater_core />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script setup lang="ts">
|
||||
import settings_updater from "./settings_updater.vue";
|
||||
import settings_station from "./settings_station.vue";
|
||||
import settings_gui from "./settings_gui.vue";
|
||||
import settings_chat from "./settings_chat.vue";
|
||||
|
@ -23,20 +22,6 @@ import settings_exp from "./settings_exp.vue";
|
|||
<li class="nav-item" role="presentation">
|
||||
<button
|
||||
class="nav-link active"
|
||||
id="updater-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#updater"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="home"
|
||||
aria-selected="true"
|
||||
>
|
||||
Updater
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button
|
||||
class="nav-link"
|
||||
id="station-tab"
|
||||
data-bs-toggle="tab"
|
||||
data-bs-target="#station"
|
||||
|
@ -141,23 +126,10 @@ import settings_exp from "./settings_exp.vue";
|
|||
>
|
||||
<!-- SETTINGS Nav Tab panes -->
|
||||
|
||||
<!-- Updater tab contents-->
|
||||
<div class="tab-content">
|
||||
<div
|
||||
class="tab-pane active"
|
||||
id="updater"
|
||||
role="tabpanel"
|
||||
aria-labelledby="updater-tab"
|
||||
tabindex="0"
|
||||
>
|
||||
<settings_updater />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Station tab contents-->
|
||||
<div class="tab-content">
|
||||
<div
|
||||
class="tab-pane"
|
||||
class="tab-pane active"
|
||||
id="station"
|
||||
role="tabpanel"
|
||||
aria-labelledby="station-tab"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { settingsStore as settings, onChange } from "../store/settingsStore.js";
|
||||
import { serialDeviceOptions } from "../js/deviceFormHelper";
|
||||
import { useSerialStore } from "../store/serialStore";
|
||||
const serialStore = useSerialStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -21,13 +22,13 @@ import { serialDeviceOptions } from "../js/deviceFormHelper";
|
|||
<div class="input-group input-group-sm mb-1">
|
||||
<span class="input-group-text" style="width: 180px">Rigctld port</span>
|
||||
<input
|
||||
type="text"
|
||||
type="number"
|
||||
class="form-control"
|
||||
placeholder="rigctld port"
|
||||
id="hamlib_rigctld_port"
|
||||
aria-label="Device Port"
|
||||
@change="onChange"
|
||||
v-model="settings.remote.RIGCTLD.port"
|
||||
v-model.number="settings.remote.RIGCTLD.port"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -44,7 +45,7 @@ import { serialDeviceOptions } from "../js/deviceFormHelper";
|
|||
@change="onChange"
|
||||
v-model.number="settings.remote.RADIO.model_id"
|
||||
>
|
||||
<option selected value="-- ignore --">-- ignore --</option>
|
||||
<option selected value="0">-- ignore --</option>
|
||||
<option value="1">Hamlib Dummy</option>
|
||||
<option value="2">Hamlib NET rigctl</option>
|
||||
<option value="4">FLRig FLRig</option>
|
||||
|
@ -334,18 +335,16 @@ import { serialDeviceOptions } from "../js/deviceFormHelper";
|
|||
<span class="input-group-text" style="width: 180px">Radio port</span>
|
||||
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
aria-label=".form-select-sm"
|
||||
id="hamlib_deviceport"
|
||||
style="width: 7rem"
|
||||
@change="onChange"
|
||||
v-model="settings.remote.RADIO.serial_port"
|
||||
class="form-select form-select-sm"
|
||||
>
|
||||
<option
|
||||
v-for="option in serialDeviceOptions()"
|
||||
v-bind:value="option.port"
|
||||
v-for="device in serialStore.serialDevices"
|
||||
:value="device.port"
|
||||
:key="device.port"
|
||||
>
|
||||
{{ option.description }}
|
||||
{{ device.description }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
@ -422,18 +421,18 @@ import { serialDeviceOptions } from "../js/deviceFormHelper";
|
|||
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<span class="input-group-text" style="width: 180px">PTT device port</span>
|
||||
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
aria-label=".form-select-sm"
|
||||
id="hamlib_ptt_port"
|
||||
@change="onChange"
|
||||
v-model="settings.remote.RADIO.ptt_port"
|
||||
class="form-select form-select-sm"
|
||||
>
|
||||
<option
|
||||
v-for="option in serialDeviceOptions()"
|
||||
v-bind:value="option.port"
|
||||
v-for="device in serialStore.serialDevices"
|
||||
:value="device.port"
|
||||
:key="device.port"
|
||||
>
|
||||
{{ option.description }}
|
||||
{{ device.description }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,9 @@ import { useStateStore } from "../store/stateStore.js";
|
|||
const state = useStateStore(pinia);
|
||||
|
||||
import { startModem, stopModem } from "../js/api.js";
|
||||
import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
|
||||
|
||||
import { useAudioStore } from "../store/audioStore";
|
||||
const audioStore = useAudioStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -74,8 +76,8 @@ import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
|
|||
@change="onChange"
|
||||
v-model="settings.remote.AUDIO.input_device"
|
||||
>
|
||||
<option v-for="option in audioInputOptions()" v-bind:value="option.id">
|
||||
{{ option.name }} [{{ option.api }}]
|
||||
<option v-for="device in audioStore.audioInputs" :value="device.id">
|
||||
{{ device.name }} [{{ device.api }}]
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
@ -89,11 +91,12 @@ import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
|
|||
@change="onChange"
|
||||
v-model="settings.remote.AUDIO.output_device"
|
||||
>
|
||||
<option v-for="option in audioOutputOptions()" v-bind:value="option.id">
|
||||
{{ option.name }} [{{ option.api }}]
|
||||
<option v-for="device in audioStore.audioOutputs" :value="device.id">
|
||||
{{ device.name }} [{{ device.api }}]
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Audio rx level-->
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<span class="input-group-text w-25">RX Audio Level</span>
|
||||
|
@ -162,70 +165,19 @@ import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
|
|||
</div>
|
||||
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<label class="input-group-text w-25">Tuning range</label>
|
||||
<label class="input-group-text">fmin</label>
|
||||
<label class="input-group-text w-50">Maximum used bandwidth</label>
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
id="tuning_range_fmin"
|
||||
id="maximum_bandwidth"
|
||||
@change="onChange"
|
||||
v-model.number="settings.remote.MODEM.tuning_range_fmin"
|
||||
v-model.number="settings.remote.MODEM.maximum_bandwidth"
|
||||
>
|
||||
<option value="-50">-50</option>
|
||||
<option value="-100">-100</option>
|
||||
<option value="-150">-150</option>
|
||||
<option value="-200">-200</option>
|
||||
<option value="-250">-250</option>
|
||||
</select>
|
||||
<label class="input-group-text">fmax</label>
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
id="tuning_range_fmax"
|
||||
@change="onChange"
|
||||
v-model.number="settings.remote.MODEM.tuning_range_fmax"
|
||||
>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="150">150</option>
|
||||
<option value="200">200</option>
|
||||
<option value="250">250</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<span class="input-group-text w-50">Beacon interval</span>
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
aria-label=".form-select-sm"
|
||||
id="beaconInterval"
|
||||
style="width: 6rem"
|
||||
@change="onChange"
|
||||
v-model.number="settings.remote.MODEM.beacon_interval"
|
||||
>
|
||||
<option value="60">60 secs</option>
|
||||
<option value="90">90 secs</option>
|
||||
<option value="120">2 mins</option>
|
||||
<option selected value="300">5 mins</option>
|
||||
<option value="600">10 mins</option>
|
||||
<option value="900">15 mins</option>
|
||||
<option value="1800">30 mins</option>
|
||||
<option value="3600">60 mins</option>
|
||||
<option value="250">250 Hz</option>
|
||||
<option value="563">563 Hz</option>
|
||||
<option value="1700">1700 Hz</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<label class="input-group-text w-50">Enable 250Hz bandwidth mode</label>
|
||||
<label class="input-group-text w-50">
|
||||
<div class="form-check form-switch form-check-inline">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="250HzModeSwitch"
|
||||
v-model="settings.remote.MODEM.enable_low_bandwidth_mode"
|
||||
@change="onChange"
|
||||
/>
|
||||
<label class="form-check-label" for="250HzModeSwitch">250Hz</label>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<label class="input-group-text w-50">Respond to CQ</label>
|
||||
<label class="input-group-text w-50">
|
||||
|
@ -241,27 +193,4 @@ import { audioInputOptions, audioOutputOptions } from "../js/deviceFormHelper";
|
|||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="input-group input-group-sm mb-1">
|
||||
<label class="input-group-text w-50">RX buffer size</label>
|
||||
<label class="input-group-text w-50">
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
id="rx_buffer_size"
|
||||
@change="onChange"
|
||||
v-model.number="settings.remote.MODEM.rx_buffer_size"
|
||||
>
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="4">4</option>
|
||||
<option value="8">8</option>
|
||||
<option value="16">16</option>
|
||||
<option value="32">32</option>
|
||||
<option value="64">64</option>
|
||||
<option value="128">128</option>
|
||||
<option value="256">256</option>
|
||||
<option value="512">512</option>
|
||||
<option value="1024">1024</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { settingsStore as settings, onChange } from "../store/settingsStore.js";
|
||||
import { serialDeviceOptions } from "../js/deviceFormHelper";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import settings_updater_core from "./settings_updater_core.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
The updater might not working, yet! Please update manually if you are running into problems!
|
||||
</div>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
The updater doesnt contain the server related parts, yet! We are discussing this topic actually, feel free contributing with your opinion on Discord!
|
||||
</div>
|
||||
|
||||
<settings_updater_core />
|
||||
</div>
|
||||
</template>
|
|
@ -1,102 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { setActivePinia } from "pinia";
|
||||
import pinia from "../store/index";
|
||||
setActivePinia(pinia);
|
||||
|
||||
import { useStateStore } from "../store/stateStore.js";
|
||||
import { settingsStore } from "../store/settingsStore";
|
||||
import { onMounted } from "vue";
|
||||
import { ipcRenderer } from "electron";
|
||||
const state = useStateStore(pinia);
|
||||
onMounted(() => {
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
// we are using this area for implementing the electron runUpdater
|
||||
// we need access to DOM for displaying updater results in GUI
|
||||
// close app, update and restart
|
||||
document
|
||||
.getElementById("update_and_install")
|
||||
.addEventListener("click", () => {
|
||||
ipcRenderer.send("request-restart-and-install-update");
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="card m-2">
|
||||
<div class="card-header p-1 d-flex">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-1">
|
||||
<i class="bi bi-cloud-download" style="font-size: 1.2rem"></i>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<strong class="fs-5">Updater</strong>
|
||||
</div>
|
||||
<div class="col-7">
|
||||
<div class="progress w-100 ms-1 m-1">
|
||||
<div
|
||||
class="progress-bar"
|
||||
style="width: 0%"
|
||||
role="progressbar"
|
||||
id="UpdateProgressBar"
|
||||
aria-valuenow="0"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
>
|
||||
<span id="UpdateProgressInfo"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-1 text-end">
|
||||
<button
|
||||
type="button"
|
||||
id="openHelpModalUpdater"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#updaterHelpModal"
|
||||
class="btn m-0 p-0 border-0"
|
||||
>
|
||||
<i class="bi bi-question-circle" style="font-size: 1rem"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-2 mb-1">
|
||||
<button
|
||||
class="btn btn-secondary btn-sm ms-1 me-1"
|
||||
id="updater_channel"
|
||||
type="button"
|
||||
disabled
|
||||
>
|
||||
Update channel: {{ settingsStore.local.update_channel }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm ms-1"
|
||||
id="updater_status"
|
||||
type="button"
|
||||
disabled
|
||||
>
|
||||
...
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary btn-sm ms-1"
|
||||
id="updater_changelog"
|
||||
type="button"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#updaterReleaseNotes"
|
||||
>
|
||||
Changelog
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary btn-sm ms-1"
|
||||
id="update_and_install"
|
||||
type="button"
|
||||
style="display: none"
|
||||
>
|
||||
Install & Restart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,53 +0,0 @@
|
|||
import { getAudioDevices, getSerialDevices } from "./api";
|
||||
|
||||
let audioDevices = await getAudioDevices();
|
||||
let serialDevices = await getSerialDevices();
|
||||
|
||||
//Dummy device data sent if unable to get devices from modem to prevent GUI crash
|
||||
const skel = JSON.parse(`
|
||||
[{
|
||||
"api": "MME",
|
||||
"id": "0000",
|
||||
"name": "No devices received from modem",
|
||||
"native_index": 0
|
||||
}]`);
|
||||
|
||||
export function loadAudioDevices() {
|
||||
getAudioDevices().then((devices) => {
|
||||
audioDevices = devices;
|
||||
});
|
||||
}
|
||||
|
||||
export function loadSerialDevices() {
|
||||
getSerialDevices().then((devices) => {
|
||||
serialDevices = devices;
|
||||
});
|
||||
}
|
||||
|
||||
export function audioInputOptions() {
|
||||
if (audioDevices === undefined) {
|
||||
return skel;
|
||||
}
|
||||
return audioDevices.in;
|
||||
}
|
||||
|
||||
export function audioOutputOptions() {
|
||||
if (audioDevices === undefined) {
|
||||
return skel;
|
||||
}
|
||||
|
||||
return audioDevices.out;
|
||||
}
|
||||
|
||||
export function serialDeviceOptions() {
|
||||
//Return ignore option if no serialDevices
|
||||
if (serialDevices === undefined)
|
||||
return [{ description: "-- ignore --", port: "ignore" }];
|
||||
|
||||
if (serialDevices.findIndex((device) => device.port == "ignore") == -1) {
|
||||
//Add an ignore option for rig and ptt for transceivers that don't require them
|
||||
serialDevices.push({ description: "-- ignore --", port: "ignore" });
|
||||
}
|
||||
|
||||
return serialDevices;
|
||||
}
|
|
@ -8,16 +8,15 @@ import {
|
|||
} from "./chatHandler";
|
||||
*/
|
||||
import { displayToast } from "./popupHandler";
|
||||
import {
|
||||
getFreedataMessages,
|
||||
getConfig,
|
||||
getAudioDevices,
|
||||
getSerialDevices,
|
||||
getModemState,
|
||||
} from "./api";
|
||||
import { getFreedataMessages, getModemState, getAudioDevices } from "./api";
|
||||
import { processFreedataMessages } from "./messagesHandler.ts";
|
||||
import { processRadioStatus } from "./radioHandler.ts";
|
||||
|
||||
import { useAudioStore } from "../store/audioStore";
|
||||
const audioStore = useAudioStore();
|
||||
import { useSerialStore } from "../store/serialStore";
|
||||
const serialStore = useSerialStore();
|
||||
|
||||
// ----------------- init pinia stores -------------
|
||||
import { setActivePinia } from "pinia";
|
||||
import pinia from "../store/index";
|
||||
|
@ -30,6 +29,17 @@ import {
|
|||
getRemote,
|
||||
} from "../store/settingsStore.js";
|
||||
|
||||
export function loadAllData() {
|
||||
getModemState();
|
||||
getRemote();
|
||||
getOverallHealth();
|
||||
audioStore.loadAudioDevices();
|
||||
serialStore.loadSerialDevices();
|
||||
getFreedataMessages();
|
||||
processFreedataMessages();
|
||||
processRadioStatus();
|
||||
}
|
||||
|
||||
export function connectionFailed(endpoint, event) {
|
||||
stateStore.modem_connection = "disconnected";
|
||||
}
|
||||
|
@ -95,12 +105,7 @@ export function eventDispatcher(data) {
|
|||
switch (data["modem"]) {
|
||||
case "started":
|
||||
displayToast("success", "bi-arrow-left-right", "Modem started", 5000);
|
||||
getModemState();
|
||||
getConfig();
|
||||
getAudioDevices();
|
||||
getSerialDevices();
|
||||
getFreedataMessages();
|
||||
processRadioStatus();
|
||||
loadAllData();
|
||||
return;
|
||||
|
||||
case "stopped":
|
||||
|
@ -109,12 +114,7 @@ export function eventDispatcher(data) {
|
|||
|
||||
case "restarted":
|
||||
displayToast("secondary", "bi-bootstrap-reboot", "Modem restarted", 5000);
|
||||
getModemState();
|
||||
getConfig();
|
||||
getAudioDevices();
|
||||
getSerialDevices();
|
||||
getFreedataMessages();
|
||||
processRadioStatus();
|
||||
loadAllData();
|
||||
return;
|
||||
|
||||
case "failed":
|
||||
|
@ -135,19 +135,7 @@ export function eventDispatcher(data) {
|
|||
displayToast("success", "bi-ethernet", message, 5000);
|
||||
stateStore.modem_connection = "connected";
|
||||
|
||||
getRemote().then(() => {
|
||||
//initConnections();
|
||||
getModemState();
|
||||
});
|
||||
|
||||
//getConfig();
|
||||
getModemState();
|
||||
getOverallHealth();
|
||||
getAudioDevices();
|
||||
getSerialDevices();
|
||||
getFreedataMessages();
|
||||
processFreedataMessages();
|
||||
processRadioStatus();
|
||||
loadAllData();
|
||||
|
||||
return;
|
||||
|
||||
|
@ -190,6 +178,12 @@ export function eventDispatcher(data) {
|
|||
100;
|
||||
stateStore.arq_total_bytes =
|
||||
data["arq-transfer-outbound"].received_bytes;
|
||||
stateStore.arq_speed_list_timestamp =
|
||||
data["arq-transfer-outbound"].statistics.time_histogram;
|
||||
stateStore.arq_speed_list_bpm =
|
||||
data["arq-transfer-outbound"].statistics.bpm_histogram;
|
||||
stateStore.arq_speed_list_snr =
|
||||
data["arq-transfer-outbound"].statistics.snr_histogram;
|
||||
return;
|
||||
|
||||
case "ABORTING":
|
||||
|
@ -232,6 +226,13 @@ export function eventDispatcher(data) {
|
|||
stateStore.dxcallsign = data["arq-transfer-inbound"].dxcall;
|
||||
stateStore.arq_transmission_percent = 0;
|
||||
stateStore.arq_total_bytes = 0;
|
||||
stateStore.arq_speed_list_timestamp =
|
||||
data["arq-transfer-inbound"].statistics.time_histogram;
|
||||
stateStore.arq_speed_list_bpm =
|
||||
data["arq-transfer-inbound"].statistics.bpm_histogram;
|
||||
stateStore.arq_speed_list_snr =
|
||||
data["arq-transfer-inbound"].statistics.snr_histogram;
|
||||
|
||||
return;
|
||||
|
||||
case "OPEN_ACK_SENT":
|
||||
|
|
|
@ -83,6 +83,7 @@ function createSortedMessagesList(data: {
|
|||
|
||||
export function newMessage(dxcall, body, attachments) {
|
||||
sendFreedataMessage(dxcall, body, attachments);
|
||||
chatStore.triggerScrollToBottom();
|
||||
}
|
||||
|
||||
/* ------ TEMPORARY DUMMY FUNCTIONS --- */
|
||||
|
|
39
gui/src/store/audioStore.js
Normal file
39
gui/src/store/audioStore.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { getAudioDevices } from "../js/api";
|
||||
import { ref } from "vue";
|
||||
|
||||
// Define skel fallback data
|
||||
const skel = [
|
||||
{
|
||||
api: "ERR",
|
||||
id: "0000",
|
||||
name: "No devices received from modem",
|
||||
native_index: 0,
|
||||
},
|
||||
];
|
||||
|
||||
export const useAudioStore = defineStore("audioStore", () => {
|
||||
const audioInputs = ref([]);
|
||||
const audioOutputs = ref([]);
|
||||
|
||||
const loadAudioDevices = async () => {
|
||||
try {
|
||||
const devices = await getAudioDevices();
|
||||
// Check if devices are valid and have entries, otherwise use skel
|
||||
audioInputs.value = devices && devices.in.length > 0 ? devices.in : skel;
|
||||
audioOutputs.value =
|
||||
devices && devices.out.length > 0 ? devices.out : skel;
|
||||
} catch (error) {
|
||||
console.error("Failed to load audio devices:", error);
|
||||
// Use skel as fallback in case of error
|
||||
audioInputs.value = skel;
|
||||
audioOutputs.value = skel;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
audioInputs,
|
||||
audioOutputs,
|
||||
loadAudioDevices,
|
||||
};
|
||||
});
|
|
@ -7,6 +7,14 @@ export const useChatStore = defineStore("chatStore", () => {
|
|||
var newChatCallsign = ref();
|
||||
var newChatMessage = ref();
|
||||
|
||||
/* ------------------------------------------------ */
|
||||
// Scroll to bottom functions
|
||||
const scrollTrigger = ref(0);
|
||||
|
||||
function triggerScrollToBottom() {
|
||||
scrollTrigger.value++;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------ */
|
||||
|
||||
var chat_filter = ref([
|
||||
|
@ -92,5 +100,7 @@ export const useChatStore = defineStore("chatStore", () => {
|
|||
arq_speed_list_bpm,
|
||||
arq_speed_list_snr,
|
||||
arq_speed_list_timestamp,
|
||||
scrollTrigger,
|
||||
triggerScrollToBottom,
|
||||
};
|
||||
});
|
||||
|
|
38
gui/src/store/serialStore.js
Normal file
38
gui/src/store/serialStore.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { getSerialDevices } from "../js/api"; // Make sure this points to the correct file
|
||||
import { ref } from "vue";
|
||||
|
||||
// Define "skel" fallback data for serial devices
|
||||
const skelSerial = [
|
||||
{
|
||||
description: "No devices received from modem",
|
||||
port: "ignore", // Using "ignore" as a placeholder value
|
||||
},
|
||||
];
|
||||
|
||||
export const useSerialStore = defineStore("serialStore", () => {
|
||||
const serialDevices = ref([]);
|
||||
|
||||
const loadSerialDevices = async () => {
|
||||
try {
|
||||
const devices = await getSerialDevices();
|
||||
// Check if devices are valid and have entries, otherwise use skelSerial
|
||||
serialDevices.value =
|
||||
devices && devices.length > 0 ? devices : skelSerial;
|
||||
} catch (error) {
|
||||
console.error("Failed to load serial devices:", error);
|
||||
// Use skelSerial as fallback in case of error
|
||||
serialDevices.value = skelSerial;
|
||||
}
|
||||
|
||||
// Ensure the "-- ignore --" option is always available
|
||||
if (!serialDevices.value.some((device) => device.port === "ignore")) {
|
||||
serialDevices.value.push({ description: "-- ignore --", port: "ignore" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
serialDevices,
|
||||
loadSerialDevices,
|
||||
};
|
||||
});
|
|
@ -54,16 +54,11 @@ const defaultConfig = {
|
|||
enable_protocol: false,
|
||||
},
|
||||
MODEM: {
|
||||
enable_fsk: false,
|
||||
enable_low_bandwidth_mode: false,
|
||||
respond_to_cq: false,
|
||||
rx_buffer_size: 0,
|
||||
tuning_range_fmax: 0,
|
||||
tuning_range_fmin: 0,
|
||||
tx_delay: 0,
|
||||
beacon_interval: 0,
|
||||
enable_hamc: false,
|
||||
enable_morse_identifier: false,
|
||||
maximum_bandwidth: 3000,
|
||||
},
|
||||
RADIO: {
|
||||
control: "disabled",
|
||||
|
|
|
@ -53,6 +53,7 @@ export const useStateStore = defineStore("stateStore", () => {
|
|||
var arq_speed_list_bpm = ref([]);
|
||||
var arq_speed_list_snr = ref([]);
|
||||
|
||||
/* TODO Those 3 can be removed I guess , DJ2LS*/
|
||||
var arq_seconds_until_finish = ref();
|
||||
var arq_seconds_until_timeout = ref();
|
||||
var arq_seconds_until_timeout_percent = ref();
|
||||
|
|
|
@ -1,5 +1,16 @@
|
|||
import re
|
||||
|
||||
|
||||
def validate_remote_config(config):
|
||||
if not config:
|
||||
return
|
||||
|
||||
mygrid = config["STATION"]["mygrid"]
|
||||
if len(mygrid) != 6:
|
||||
raise ValueError(f"Gridsquare must be 6 characters!")
|
||||
|
||||
return True
|
||||
|
||||
def validate_freedata_callsign(callsign):
|
||||
#regexp = "^[a-zA-Z]+\d+\w+-\d{1,2}$"
|
||||
regexp = "^[A-Za-z0-9]{1,7}-[0-9]{1,3}$" # still broken - we need to allow all ssids form 0 - 255
|
||||
|
|
|
@ -11,6 +11,7 @@ class ARQ_SESSION_TYPES(Enum):
|
|||
raw_lzma = 10
|
||||
raw_gzip = 11
|
||||
p2pmsg_lzma = 20
|
||||
p2p_connection = 30
|
||||
|
||||
class ARQDataTypeHandler:
|
||||
def __init__(self, event_manager, state_manager):
|
||||
|
@ -43,6 +44,12 @@ class ARQDataTypeHandler:
|
|||
'failed' : self.failed_p2pmsg_lzma,
|
||||
'transmitted': self.transmitted_p2pmsg_lzma,
|
||||
},
|
||||
ARQ_SESSION_TYPES.p2p_connection: {
|
||||
'prepare': self.prepare_p2p_connection,
|
||||
'handle': self.handle_p2p_connection,
|
||||
'failed': self.failed_p2p_connection,
|
||||
'transmitted': self.transmitted_p2p_connection,
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
@ -52,23 +59,23 @@ class ARQDataTypeHandler:
|
|||
return session_type
|
||||
return None
|
||||
|
||||
def dispatch(self, type_byte: int, data: bytearray):
|
||||
def dispatch(self, type_byte: int, data: bytearray, statistics: dict):
|
||||
session_type = self.get_session_type_from_value(type_byte)
|
||||
|
||||
self.state_manager.setARQ(False)
|
||||
|
||||
if session_type and session_type in self.handlers and 'handle' in self.handlers[session_type]:
|
||||
return self.handlers[session_type]['handle'](data)
|
||||
return self.handlers[session_type]['handle'](data, statistics)
|
||||
else:
|
||||
self.log(f"Unknown handling endpoint for type: {type_byte}", isWarning=True)
|
||||
|
||||
def failed(self, type_byte: int, data: bytearray):
|
||||
def failed(self, type_byte: int, data: bytearray, statistics: dict):
|
||||
session_type = self.get_session_type_from_value(type_byte)
|
||||
|
||||
self.state_manager.setARQ(False)
|
||||
|
||||
if session_type in self.handlers and 'failed' in self.handlers[session_type]:
|
||||
return self.handlers[session_type]['failed'](data)
|
||||
return self.handlers[session_type]['failed'](data, statistics)
|
||||
else:
|
||||
self.log(f"Unknown handling endpoint: {session_type}", isWarning=True)
|
||||
|
||||
|
@ -78,13 +85,13 @@ class ARQDataTypeHandler:
|
|||
else:
|
||||
self.log(f"Unknown preparation endpoint: {session_type}", isWarning=True)
|
||||
|
||||
def transmitted(self, type_byte: int, data: bytearray):
|
||||
def transmitted(self, type_byte: int, data: bytearray, statistics: dict):
|
||||
session_type = self.get_session_type_from_value(type_byte)
|
||||
|
||||
self.state_manager.setARQ(False)
|
||||
|
||||
if session_type in self.handlers and 'transmitted' in self.handlers[session_type]:
|
||||
return self.handlers[session_type]['transmitted'](data)
|
||||
return self.handlers[session_type]['transmitted'](data, statistics)
|
||||
else:
|
||||
self.log(f"Unknown handling endpoint: {session_type}", isWarning=True)
|
||||
|
||||
|
@ -97,14 +104,14 @@ class ARQDataTypeHandler:
|
|||
self.log(f"Preparing uncompressed data: {len(data)} Bytes")
|
||||
return data
|
||||
|
||||
def handle_raw(self, data):
|
||||
def handle_raw(self, data, statistics):
|
||||
self.log(f"Handling uncompressed data: {len(data)} Bytes")
|
||||
return data
|
||||
|
||||
def failed_raw(self, data):
|
||||
def failed_raw(self, data, statistics):
|
||||
return
|
||||
|
||||
def transmitted_raw(self, data):
|
||||
def transmitted_raw(self, data, statistics):
|
||||
return data
|
||||
|
||||
def prepare_raw_lzma(self, data):
|
||||
|
@ -112,15 +119,15 @@ class ARQDataTypeHandler:
|
|||
self.log(f"Preparing LZMA compressed data: {len(data)} Bytes >>> {len(compressed_data)} Bytes")
|
||||
return compressed_data
|
||||
|
||||
def handle_raw_lzma(self, data):
|
||||
def handle_raw_lzma(self, data, statistics):
|
||||
decompressed_data = lzma.decompress(data)
|
||||
self.log(f"Handling LZMA compressed data: {len(decompressed_data)} Bytes from {len(data)} Bytes")
|
||||
return decompressed_data
|
||||
|
||||
def failed_raw_lzma(self, data):
|
||||
def failed_raw_lzma(self, data, statistics):
|
||||
return
|
||||
|
||||
def transmitted_raw_lzma(self, data):
|
||||
def transmitted_raw_lzma(self, data, statistics):
|
||||
decompressed_data = lzma.decompress(data)
|
||||
return decompressed_data
|
||||
|
||||
|
@ -129,15 +136,15 @@ class ARQDataTypeHandler:
|
|||
self.log(f"Preparing GZIP compressed data: {len(data)} Bytes >>> {len(compressed_data)} Bytes")
|
||||
return compressed_data
|
||||
|
||||
def handle_raw_gzip(self, data):
|
||||
def handle_raw_gzip(self, data, statistics):
|
||||
decompressed_data = gzip.decompress(data)
|
||||
self.log(f"Handling GZIP compressed data: {len(decompressed_data)} Bytes from {len(data)} Bytes")
|
||||
return decompressed_data
|
||||
|
||||
def failed_raw_gzip(self, data):
|
||||
def failed_raw_gzip(self, data, statistics):
|
||||
return
|
||||
|
||||
def transmitted_raw_gzip(self, data):
|
||||
def transmitted_raw_gzip(self, data, statistics):
|
||||
decompressed_data = gzip.decompress(data)
|
||||
return decompressed_data
|
||||
|
||||
|
@ -146,19 +153,51 @@ class ARQDataTypeHandler:
|
|||
self.log(f"Preparing LZMA compressed P2PMSG data: {len(data)} Bytes >>> {len(compressed_data)} Bytes")
|
||||
return compressed_data
|
||||
|
||||
def handle_p2pmsg_lzma(self, data):
|
||||
def handle_p2pmsg_lzma(self, data, statistics):
|
||||
decompressed_data = lzma.decompress(data)
|
||||
self.log(f"Handling LZMA compressed P2PMSG data: {len(decompressed_data)} Bytes from {len(data)} Bytes")
|
||||
message_received(self.event_manager, self.state_manager, decompressed_data)
|
||||
message_received(self.event_manager, self.state_manager, decompressed_data, statistics)
|
||||
return decompressed_data
|
||||
|
||||
def failed_p2pmsg_lzma(self, data):
|
||||
def failed_p2pmsg_lzma(self, data, statistics):
|
||||
decompressed_data = lzma.decompress(data)
|
||||
self.log(f"Handling failed LZMA compressed P2PMSG data: {len(decompressed_data)} Bytes from {len(data)} Bytes", isWarning=True)
|
||||
message_failed(self.event_manager, self.state_manager, decompressed_data)
|
||||
message_failed(self.event_manager, self.state_manager, decompressed_data, statistics)
|
||||
return decompressed_data
|
||||
|
||||
def transmitted_p2pmsg_lzma(self, data):
|
||||
def transmitted_p2pmsg_lzma(self, data, statistics):
|
||||
decompressed_data = lzma.decompress(data)
|
||||
message_transmitted(self.event_manager, self.state_manager, decompressed_data)
|
||||
return decompressed_data
|
||||
message_transmitted(self.event_manager, self.state_manager, decompressed_data, statistics)
|
||||
return decompressed_data
|
||||
|
||||
|
||||
def prepare_p2p_connection(self, data):
|
||||
compressed_data = gzip.compress(data)
|
||||
self.log(f"Preparing gzip compressed P2P_CONNECTION data: {len(data)} Bytes >>> {len(compressed_data)} Bytes")
|
||||
print(self.state_manager.p2p_connection_sessions)
|
||||
return compressed_data
|
||||
|
||||
def handle_p2p_connection(self, data, statistics):
|
||||
decompressed_data = gzip.decompress(data)
|
||||
self.log(f"Handling gzip compressed P2P_CONNECTION data: {len(decompressed_data)} Bytes from {len(data)} Bytes")
|
||||
print(self.state_manager.p2p_connection_sessions)
|
||||
print(decompressed_data)
|
||||
print(self.state_manager.p2p_connection_sessions)
|
||||
for session_id in self.state_manager.p2p_connection_sessions:
|
||||
print(session_id)
|
||||
self.state_manager.p2p_connection_sessions[session_id].received_arq(decompressed_data)
|
||||
|
||||
def failed_p2p_connection(self, data, statistics):
|
||||
decompressed_data = gzip.decompress(data)
|
||||
self.log(f"Handling failed gzip compressed P2P_CONNECTION data: {len(decompressed_data)} Bytes from {len(data)} Bytes", isWarning=True)
|
||||
print(self.state_manager.p2p_connection_sessions)
|
||||
return decompressed_data
|
||||
|
||||
def transmitted_p2p_connection(self, data, statistics):
|
||||
|
||||
decompressed_data = gzip.decompress(data)
|
||||
print(decompressed_data)
|
||||
print(self.state_manager.p2p_connection_sessions)
|
||||
for session_id in self.state_manager.p2p_connection_sessions:
|
||||
print(session_id)
|
||||
self.state_manager.p2p_connection_sessions[session_id].transmitted_arq()
|
|
@ -1,4 +1,5 @@
|
|||
import queue, threading
|
||||
import datetime
|
||||
import threading
|
||||
import codec2
|
||||
import data_frame_factory
|
||||
import structlog
|
||||
|
@ -8,33 +9,36 @@ import time
|
|||
from arq_data_type_handler import ARQDataTypeHandler
|
||||
|
||||
|
||||
class ARQSession():
|
||||
class ARQSession:
|
||||
|
||||
SPEED_LEVEL_DICT = {
|
||||
0: {
|
||||
'mode': codec2.FREEDV_MODE.datac4,
|
||||
'min_snr': -10,
|
||||
'duration_per_frame': 5.17,
|
||||
'bandwidth': 250,
|
||||
},
|
||||
1: {
|
||||
'mode': codec2.FREEDV_MODE.datac3,
|
||||
'min_snr': 0,
|
||||
'duration_per_frame': 3.19,
|
||||
'bandwidth': 563,
|
||||
},
|
||||
2: {
|
||||
'mode': codec2.FREEDV_MODE.datac1,
|
||||
'min_snr': 3,
|
||||
'duration_per_frame': 4.18,
|
||||
'bandwidth': 1700,
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, config: dict, modem, dxcall: str):
|
||||
def __init__(self, config: dict, modem, dxcall: str, state_manager):
|
||||
self.logger = structlog.get_logger(type(self).__name__)
|
||||
self.config = config
|
||||
|
||||
self.event_manager: EventManager = modem.event_manager
|
||||
self.states = modem.states
|
||||
|
||||
#self.states = modem.states
|
||||
self.states = state_manager
|
||||
self.states.setARQ(True)
|
||||
|
||||
self.snr = []
|
||||
|
@ -44,6 +48,8 @@ class ARQSession():
|
|||
|
||||
self.modem = modem
|
||||
self.speed_level = 0
|
||||
self.previous_speed_level = 0
|
||||
|
||||
self.frames_per_burst = 1
|
||||
|
||||
self.frame_factory = data_frame_factory.DataFrameFactory(self.config)
|
||||
|
@ -55,7 +61,12 @@ class ARQSession():
|
|||
self.session_ended = 0
|
||||
self.session_max_age = 500
|
||||
|
||||
def log(self, message, isWarning = False):
|
||||
# histogram lists for storing statistics
|
||||
self.snr_histogram = []
|
||||
self.bpm_histogram = []
|
||||
self.time_histogram = []
|
||||
|
||||
def log(self, message, isWarning=False):
|
||||
msg = f"[{type(self).__name__}][id={self.id}][state={self.state}]: {message}"
|
||||
logger = self.logger.warn if isWarning else self.logger.info
|
||||
logger(msg)
|
||||
|
@ -84,47 +95,89 @@ class ARQSession():
|
|||
)
|
||||
|
||||
def set_details(self, snr, frequency_offset):
|
||||
self.snr.append(snr)
|
||||
self.snr = snr
|
||||
self.frequency_offset = frequency_offset
|
||||
|
||||
def on_frame_received(self, frame):
|
||||
self.event_frame_received.set()
|
||||
self.log(f"Received {frame['frame_type']}")
|
||||
frame_type = frame['frame_type_int']
|
||||
if self.state in self.STATE_TRANSITION:
|
||||
if frame_type in self.STATE_TRANSITION[self.state]:
|
||||
action_name = self.STATE_TRANSITION[self.state][frame_type]
|
||||
received_data, type_byte = getattr(self, action_name)(frame)
|
||||
if isinstance(received_data, bytearray) and isinstance(type_byte, int):
|
||||
self.arq_data_type_handler.dispatch(type_byte, received_data)
|
||||
|
||||
return
|
||||
if self.state in self.STATE_TRANSITION and frame_type in self.STATE_TRANSITION[self.state]:
|
||||
action_name = self.STATE_TRANSITION[self.state][frame_type]
|
||||
received_data, type_byte = getattr(self, action_name)(frame)
|
||||
if isinstance(received_data, bytearray) and isinstance(type_byte, int):
|
||||
self.arq_data_type_handler.dispatch(type_byte, received_data, self.update_histograms(len(received_data), len(received_data)))
|
||||
return
|
||||
|
||||
self.log(f"Ignoring unknown 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
|
||||
return self.session_ended < session_alivetime and self.state.name in [
|
||||
'FAILED',
|
||||
'ENDED',
|
||||
'ABORTED',
|
||||
]
|
||||
|
||||
def calculate_session_duration(self):
|
||||
if self.session_ended == 0:
|
||||
return time.time() - self.session_started
|
||||
|
||||
return self.session_ended - self.session_started
|
||||
|
||||
def calculate_session_statistics(self):
|
||||
def calculate_session_statistics(self, confirmed_bytes, total_bytes):
|
||||
duration = self.calculate_session_duration()
|
||||
total_bytes = self.total_length
|
||||
# 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)
|
||||
bytes_per_minute = int(confirmed_bytes / duration_in_minutes)
|
||||
else:
|
||||
bytes_per_minute = 0
|
||||
|
||||
# Convert histograms lists to dictionaries
|
||||
time_histogram_dict = dict(enumerate(self.time_histogram))
|
||||
snr_histogram_dict = dict(enumerate(self.snr_histogram))
|
||||
bpm_histogram_dict = dict(enumerate(self.bpm_histogram))
|
||||
|
||||
return {
|
||||
'total_bytes': total_bytes,
|
||||
'duration': duration,
|
||||
'bytes_per_minute': bytes_per_minute
|
||||
}
|
||||
'total_bytes': total_bytes,
|
||||
'duration': duration,
|
||||
'bytes_per_minute': bytes_per_minute,
|
||||
'time_histogram': time_histogram_dict,
|
||||
'snr_histogram': snr_histogram_dict,
|
||||
'bpm_histogram': bpm_histogram_dict,
|
||||
}
|
||||
|
||||
def update_histograms(self, confirmed_bytes, total_bytes):
|
||||
stats = self.calculate_session_statistics(confirmed_bytes, total_bytes)
|
||||
self.snr_histogram.append(self.snr)
|
||||
self.bpm_histogram.append(stats['bytes_per_minute'])
|
||||
self.time_histogram.append(datetime.datetime.now().isoformat())
|
||||
|
||||
# Limit the size of each histogram to the last 20 entries
|
||||
self.snr_histogram = self.snr_histogram[-20:]
|
||||
self.bpm_histogram = self.bpm_histogram[-20:]
|
||||
self.time_histogram = self.time_histogram[-20:]
|
||||
|
||||
return stats
|
||||
|
||||
def get_appropriate_speed_level(self, snr, maximum_bandwidth=None):
|
||||
if maximum_bandwidth is None:
|
||||
maximum_bandwidth = self.config['MODEM']['maximum_bandwidth']
|
||||
|
||||
# Adjust maximum_bandwidth based on special conditions or invalid configurations
|
||||
if maximum_bandwidth == 0:
|
||||
# Use the maximum available bandwidth from the speed level dictionary
|
||||
maximum_bandwidth = max(details['bandwidth'] for details in self.SPEED_LEVEL_DICT.values())
|
||||
|
||||
# Initialize appropriate_speed_level to the lowest level that meets the minimum criteria
|
||||
appropriate_speed_level = min(self.SPEED_LEVEL_DICT.keys())
|
||||
|
||||
for level, details in self.SPEED_LEVEL_DICT.items():
|
||||
if snr >= details['min_snr'] and details['bandwidth'] <= maximum_bandwidth and level > appropriate_speed_level:
|
||||
appropriate_speed_level = level
|
||||
|
||||
return appropriate_speed_level
|
||||
|
|
|
@ -18,7 +18,7 @@ class IRS_State(Enum):
|
|||
class ARQSessionIRS(arq_session.ARQSession):
|
||||
|
||||
TIMEOUT_CONNECT = 55 #14.2
|
||||
TIMEOUT_DATA = 60
|
||||
TIMEOUT_DATA = 120
|
||||
|
||||
STATE_TRANSITION = {
|
||||
IRS_State.NEW: {
|
||||
|
@ -59,8 +59,8 @@ class ARQSessionIRS(arq_session.ARQSession):
|
|||
},
|
||||
}
|
||||
|
||||
def __init__(self, config: dict, modem, dxcall: str, session_id: int):
|
||||
super().__init__(config, modem, dxcall)
|
||||
def __init__(self, config: dict, modem, dxcall: str, session_id: int, state_manager):
|
||||
super().__init__(config, modem, dxcall, state_manager)
|
||||
|
||||
self.id = session_id
|
||||
self.dxcall = dxcall
|
||||
|
@ -76,19 +76,15 @@ class ARQSessionIRS(arq_session.ARQSession):
|
|||
self.received_bytes = 0
|
||||
self.received_crc = None
|
||||
|
||||
self.transmitted_acks = 0
|
||||
self.maximum_bandwidth = 0
|
||||
|
||||
self.abort = False
|
||||
|
||||
def set_decode_mode(self):
|
||||
self.modem.demodulator.set_decode_mode(self.get_mode_by_speed_level(self.speed_level))
|
||||
|
||||
def all_data_received(self):
|
||||
return self.total_length == self.received_bytes
|
||||
|
||||
def final_crc_matches(self) -> bool:
|
||||
match = self.total_crc == helpers.get_crc_32(bytes(self.received_data)).hex()
|
||||
return match
|
||||
return self.total_crc == helpers.get_crc_32(bytes(self.received_data)).hex()
|
||||
|
||||
def transmit_and_wait(self, frame, timeout, mode):
|
||||
self.event_frame_received.clear()
|
||||
|
@ -104,13 +100,19 @@ class ARQSessionIRS(arq_session.ARQSession):
|
|||
thread_wait.start()
|
||||
|
||||
def send_open_ack(self, open_frame):
|
||||
self.maximum_bandwidth = open_frame['maximum_bandwidth']
|
||||
# check for maximum bandwidth. If ISS bandwidth is higher than own, then use own
|
||||
if open_frame['maximum_bandwidth'] > self.config['MODEM']['maximum_bandwidth']:
|
||||
self.maximum_bandwidth = self.config['MODEM']['maximum_bandwidth']
|
||||
|
||||
|
||||
self.event_manager.send_arq_session_new(
|
||||
False, self.id, self.dxcall, 0, self.state.name)
|
||||
ack_frame = self.frame_factory.build_arq_session_open_ack(
|
||||
self.id,
|
||||
self.dxcall,
|
||||
self.version,
|
||||
self.snr[0], flag_abort=self.abort)
|
||||
self.snr, flag_abort=self.abort)
|
||||
self.launch_transmit_and_wait(ack_frame, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling)
|
||||
if not self.abort:
|
||||
self.set_state(IRS_State.OPEN_ACK_SENT)
|
||||
|
@ -124,14 +126,13 @@ class ARQSessionIRS(arq_session.ARQSession):
|
|||
self.dx_snr.append(info_frame['snr'])
|
||||
self.type_byte = info_frame['type']
|
||||
|
||||
self.calibrate_speed_settings()
|
||||
|
||||
self.log(f"New transfer of {self.total_length} bytes")
|
||||
self.event_manager.send_arq_session_new(False, self.id, self.dxcall, self.total_length, self.state.name)
|
||||
|
||||
self.calibrate_speed_settings()
|
||||
self.set_decode_mode()
|
||||
|
||||
info_ack = self.frame_factory.build_arq_session_info_ack(
|
||||
self.id, self.total_crc, self.snr[0],
|
||||
self.id, self.total_crc, self.snr,
|
||||
self.speed_level, self.frames_per_burst, flag_abort=self.abort)
|
||||
self.launch_transmit_and_wait(info_ack, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling)
|
||||
if not self.abort:
|
||||
|
@ -157,23 +158,26 @@ class ARQSessionIRS(arq_session.ARQSession):
|
|||
self.received_bytes += len(data_part)
|
||||
self.log(f"Received {self.received_bytes}/{self.total_length} bytes")
|
||||
self.event_manager.send_arq_session_progress(
|
||||
False, self.id, self.dxcall, self.received_bytes, self.total_length, self.state.name)
|
||||
False, self.id, self.dxcall, self.received_bytes, self.total_length, self.state.name, self.calculate_session_statistics(self.received_bytes, self.total_length))
|
||||
|
||||
return True
|
||||
|
||||
def receive_data(self, burst_frame):
|
||||
self.process_incoming_data(burst_frame)
|
||||
self.calibrate_speed_settings()
|
||||
|
||||
# update statistics
|
||||
self.update_histograms(self.received_bytes, self.total_length)
|
||||
|
||||
if not self.all_data_received():
|
||||
self.calibrate_speed_settings(burst_frame=burst_frame)
|
||||
ack = self.frame_factory.build_arq_burst_ack(
|
||||
self.id, self.received_bytes,
|
||||
self.speed_level, self.frames_per_burst, self.snr[0], flag_abort=self.abort)
|
||||
self.id,
|
||||
self.received_bytes,
|
||||
self.speed_level,
|
||||
self.frames_per_burst,
|
||||
self.snr,
|
||||
flag_abort=self.abort
|
||||
)
|
||||
|
||||
self.set_decode_mode()
|
||||
|
||||
# increase ack counter
|
||||
# self.transmitted_acks += 1
|
||||
self.set_state(IRS_State.BURST_REPLY_SENT)
|
||||
self.launch_transmit_and_wait(ack, self.TIMEOUT_DATA, mode=FREEDV_MODE.signalling)
|
||||
return None, None
|
||||
|
@ -184,7 +188,7 @@ class ARQSessionIRS(arq_session.ARQSession):
|
|||
self.received_bytes,
|
||||
self.speed_level,
|
||||
self.frames_per_burst,
|
||||
self.snr[0],
|
||||
self.snr,
|
||||
flag_final=True,
|
||||
flag_checksum=True)
|
||||
self.transmit_frame(ack, mode=FREEDV_MODE.signalling)
|
||||
|
@ -192,7 +196,7 @@ class ARQSessionIRS(arq_session.ARQSession):
|
|||
self.session_ended = time.time()
|
||||
self.set_state(IRS_State.ENDED)
|
||||
self.event_manager.send_arq_session_finished(
|
||||
False, self.id, self.dxcall, True, self.state.name, data=self.received_data, statistics=self.calculate_session_statistics())
|
||||
False, self.id, self.dxcall, True, self.state.name, data=self.received_data, statistics=self.calculate_session_statistics(self.received_bytes, self.total_length))
|
||||
|
||||
return self.received_data, self.type_byte
|
||||
else:
|
||||
|
@ -201,42 +205,72 @@ class ARQSessionIRS(arq_session.ARQSession):
|
|||
self.received_bytes,
|
||||
self.speed_level,
|
||||
self.frames_per_burst,
|
||||
self.snr[0],
|
||||
self.snr,
|
||||
flag_final=True,
|
||||
flag_checksum=False)
|
||||
self.transmit_frame(ack, mode=FREEDV_MODE.signalling)
|
||||
self.log("CRC fail at the end of transmission!")
|
||||
self.transmission_failed()
|
||||
return self.transmission_failed()
|
||||
|
||||
def calibrate_speed_settings(self):
|
||||
self.speed_level = 0 # for now stay at lowest speed level
|
||||
return
|
||||
# if we have two ACKS, then consider increasing speed level
|
||||
if self.transmitted_acks >= 2:
|
||||
self.transmitted_acks = 0
|
||||
new_speed_level = min(self.speed_level + 1, len(self.SPEED_LEVEL_DICT) - 1)
|
||||
def calibrate_speed_settings(self, burst_frame=None):
|
||||
if burst_frame:
|
||||
received_speed_level = burst_frame['speed_level']
|
||||
else:
|
||||
received_speed_level = 0
|
||||
|
||||
# check first if the next mode supports the actual snr
|
||||
if self.snr[0] >= self.SPEED_LEVEL_DICT[new_speed_level]["min_snr"]:
|
||||
self.speed_level = new_speed_level
|
||||
latest_snr = self.snr if self.snr else -10
|
||||
appropriate_speed_level = self.get_appropriate_speed_level(latest_snr, self.maximum_bandwidth)
|
||||
modes_to_decode = {}
|
||||
|
||||
# Log the latest SNR, current, appropriate speed levels, and the previous speed level
|
||||
self.log(
|
||||
f"Latest SNR: {latest_snr}, Current Speed Level: {self.speed_level}, Appropriate Speed Level: {appropriate_speed_level}, Previous Speed Level: {self.previous_speed_level}",
|
||||
isWarning=True)
|
||||
|
||||
# Adjust the speed level by one step towards the appropriate level, if needed
|
||||
if appropriate_speed_level > self.speed_level and self.speed_level < len(self.SPEED_LEVEL_DICT) - 1:
|
||||
# we need to ensure, the received data is equal to our speed level before changing it
|
||||
if received_speed_level == self.speed_level:
|
||||
self.speed_level += 1
|
||||
elif appropriate_speed_level < self.speed_level and self.speed_level > 0:
|
||||
# we need to ensure, the received data is equal to our speed level before changing it
|
||||
if received_speed_level == self.speed_level:
|
||||
self.speed_level -= 1
|
||||
|
||||
# Always decode the current mode
|
||||
current_mode = self.get_mode_by_speed_level(self.speed_level).value
|
||||
modes_to_decode[current_mode] = True
|
||||
|
||||
# Decode the previous speed level mode
|
||||
if self.previous_speed_level != self.speed_level:
|
||||
previous_mode = self.get_mode_by_speed_level(self.previous_speed_level).value
|
||||
modes_to_decode[previous_mode] = True
|
||||
self.previous_speed_level = self.speed_level # Update the previous speed level
|
||||
|
||||
self.log(f"Modes to Decode: {list(modes_to_decode.keys())}", isWarning=True)
|
||||
# Apply the new decode mode based on the updated and previous speed levels
|
||||
self.modem.demodulator.set_decode_mode(modes_to_decode)
|
||||
|
||||
return self.speed_level
|
||||
|
||||
def abort_transmission(self):
|
||||
self.log(f"Aborting transmission... setting abort flag")
|
||||
self.log("Aborting transmission... setting abort flag")
|
||||
self.abort = True
|
||||
|
||||
def send_stop_ack(self, stop_frame):
|
||||
stop_ack = self.frame_factory.build_arq_stop_ack(self.id)
|
||||
self.launch_transmit_and_wait(stop_ack, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling)
|
||||
self.set_state(IRS_State.ABORTED)
|
||||
self.states.setARQ(False)
|
||||
self.event_manager.send_arq_session_finished(
|
||||
False, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics())
|
||||
False, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics(self.received_bytes, self.total_length))
|
||||
return None, None
|
||||
|
||||
def transmission_failed(self, irs_frame=None):
|
||||
# final function for failed transmissions
|
||||
self.session_ended = time.time()
|
||||
self.set_state(IRS_State.FAILED)
|
||||
self.log(f"Transmission failed!")
|
||||
self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,False, self.state.name, statistics=self.calculate_session_statistics())
|
||||
self.log("Transmission failed!")
|
||||
self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,False, self.state.name, statistics=self.calculate_session_statistics(self.received_bytes, self.total_length))
|
||||
self.states.setARQ(False)
|
||||
return None, None
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import threading
|
||||
import data_frame_factory
|
||||
import queue
|
||||
import random
|
||||
from codec2 import FREEDV_MODE
|
||||
from modem_frametypes import FRAME_TYPE
|
||||
|
@ -54,7 +53,7 @@ class ARQSessionISS(arq_session.ARQSession):
|
|||
}
|
||||
|
||||
def __init__(self, config: dict, modem, dxcall: str, state_manager, data: bytearray, type_byte: bytes):
|
||||
super().__init__(config, modem, dxcall)
|
||||
super().__init__(config, modem, dxcall, state_manager)
|
||||
self.state_manager = state_manager
|
||||
self.data = data
|
||||
self.total_length = len(data)
|
||||
|
@ -76,8 +75,7 @@ class ARQSessionISS(arq_session.ARQSession):
|
|||
if len(self.state_manager.arq_iss_sessions) >= 255:
|
||||
return False
|
||||
|
||||
|
||||
def transmit_wait_and_retry(self, frame_or_burst, timeout, retries, mode):
|
||||
def transmit_wait_and_retry(self, frame_or_burst, timeout, retries, mode, isARQBurst=False, ):
|
||||
while retries > 0:
|
||||
self.event_frame_received = threading.Event()
|
||||
if isinstance(frame_or_burst, list): burst = frame_or_burst
|
||||
|
@ -90,26 +88,50 @@ class ARQSessionISS(arq_session.ARQSession):
|
|||
return
|
||||
self.log("Timeout!")
|
||||
retries = retries - 1
|
||||
|
||||
|
||||
# TODO TEMPORARY TEST FOR SENDING IN LOWER SPEED LEVEL IF WE HAVE TWO FAILED TRANSMISSIONS!!!
|
||||
if retries == 8 and isARQBurst and self.speed_level > 0:
|
||||
self.log("SENDING IN FALLBACK SPEED LEVEL", isWarning=True)
|
||||
self.speed_level = 0
|
||||
self.send_data({'flag':{'ABORT': False, 'FINAL': False}, 'speed_level': self.speed_level})
|
||||
return
|
||||
|
||||
self.set_state(ISS_State.FAILED)
|
||||
self.transmission_failed()
|
||||
|
||||
def launch_twr(self, frame_or_burst, timeout, retries, mode):
|
||||
twr = threading.Thread(target = self.transmit_wait_and_retry, args=[frame_or_burst, timeout, retries, mode], daemon=True)
|
||||
def launch_twr(self, frame_or_burst, timeout, retries, mode, isARQBurst=False):
|
||||
twr = threading.Thread(target = self.transmit_wait_and_retry, args=[frame_or_burst, timeout, retries, mode, isARQBurst], daemon=True)
|
||||
twr.start()
|
||||
|
||||
def start(self):
|
||||
maximum_bandwidth = self.config['MODEM']['maximum_bandwidth']
|
||||
self.event_manager.send_arq_session_new(
|
||||
True, self.id, self.dxcall, self.total_length, self.state.name)
|
||||
session_open_frame = self.frame_factory.build_arq_session_open(self.dxcall, self.id)
|
||||
session_open_frame = self.frame_factory.build_arq_session_open(self.dxcall, self.id, maximum_bandwidth)
|
||||
self.launch_twr(session_open_frame, self.TIMEOUT_CONNECT_ACK, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)
|
||||
self.set_state(ISS_State.OPEN_SENT)
|
||||
|
||||
def set_speed_and_frames_per_burst(self, frame):
|
||||
self.speed_level = frame['speed_level']
|
||||
self.log(f"Speed level set to {self.speed_level}")
|
||||
self.frames_per_burst = frame['frames_per_burst']
|
||||
self.log(f"Frames per burst set to {self.frames_per_burst}")
|
||||
def update_speed_level(self, frame):
|
||||
self.log("---------------------------------------------------------", isWarning=True)
|
||||
|
||||
# Log the received frame for debugging
|
||||
self.log(f"Received frame: {frame}", isWarning=True)
|
||||
|
||||
# Extract the speed_level directly from the frame
|
||||
if 'speed_level' in frame:
|
||||
new_speed_level = frame['speed_level']
|
||||
# Ensure the new speed level is within the allowable range
|
||||
if 0 <= new_speed_level < len(self.SPEED_LEVEL_DICT):
|
||||
# Log the speed level change if it's different from the current speed level
|
||||
if new_speed_level != self.speed_level:
|
||||
self.log(f"Changing speed level from {self.speed_level} to {new_speed_level}", isWarning=True)
|
||||
self.speed_level = new_speed_level # Update the current speed level
|
||||
else:
|
||||
self.log("Received speed level is the same as the current speed level.", isWarning=True)
|
||||
else:
|
||||
self.log(f"Received speed level {new_speed_level} is out of allowable range.", isWarning=True)
|
||||
else:
|
||||
self.log("No speed level specified in the received frame.", isWarning=True)
|
||||
|
||||
def send_info(self, irs_frame):
|
||||
# check if we received an abort flag
|
||||
|
@ -119,7 +141,7 @@ class ARQSessionISS(arq_session.ARQSession):
|
|||
|
||||
info_frame = self.frame_factory.build_arq_session_info(self.id, self.total_length,
|
||||
helpers.get_crc_32(self.data),
|
||||
self.snr[0], self.type_byte)
|
||||
self.snr, self.type_byte)
|
||||
|
||||
self.launch_twr(info_frame, self.TIMEOUT_CONNECT_ACK, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)
|
||||
self.set_state(ISS_State.INFO_SENT)
|
||||
|
@ -127,14 +149,15 @@ class ARQSessionISS(arq_session.ARQSession):
|
|||
return None, None
|
||||
|
||||
def send_data(self, irs_frame):
|
||||
# update statistics
|
||||
self.update_histograms(self.confirmed_bytes, self.total_length)
|
||||
|
||||
self.set_speed_and_frames_per_burst(irs_frame)
|
||||
|
||||
self.update_speed_level(irs_frame)
|
||||
if 'offset' in irs_frame:
|
||||
self.confirmed_bytes = irs_frame['offset']
|
||||
self.log(f"IRS confirmed {self.confirmed_bytes}/{self.total_length} bytes")
|
||||
self.event_manager.send_arq_session_progress(
|
||||
True, self.id, self.dxcall, self.confirmed_bytes, self.total_length, self.state.name)
|
||||
True, self.id, self.dxcall, self.confirmed_bytes, self.total_length, self.state.name, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
|
||||
|
||||
# check if we received an abort flag
|
||||
if irs_frame["flag"]["ABORT"]:
|
||||
|
@ -151,14 +174,14 @@ class ARQSessionISS(arq_session.ARQSession):
|
|||
|
||||
payload_size = self.get_data_payload_size()
|
||||
burst = []
|
||||
for f in range(0, self.frames_per_burst):
|
||||
for _ in range(0, self.frames_per_burst):
|
||||
offset = self.confirmed_bytes
|
||||
payload = self.data[offset : offset + payload_size]
|
||||
data_frame = self.frame_factory.build_arq_burst_frame(
|
||||
self.SPEED_LEVEL_DICT[self.speed_level]["mode"],
|
||||
self.id, self.confirmed_bytes, payload)
|
||||
self.id, self.confirmed_bytes, payload, self.speed_level)
|
||||
burst.append(data_frame)
|
||||
self.launch_twr(burst, self.TIMEOUT_TRANSFER, self.RETRIES_CONNECT, mode='auto')
|
||||
self.launch_twr(burst, self.TIMEOUT_TRANSFER, self.RETRIES_CONNECT, mode='auto', isARQBurst=True)
|
||||
self.set_state(ISS_State.BURST_SENT)
|
||||
return None, None
|
||||
|
||||
|
@ -167,30 +190,34 @@ class ARQSessionISS(arq_session.ARQSession):
|
|||
self.session_ended = time.time()
|
||||
self.set_state(ISS_State.ENDED)
|
||||
self.log(f"All data transfered! flag_final={irs_frame['flag']['FINAL']}, flag_checksum={irs_frame['flag']['CHECKSUM']}")
|
||||
self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,True, self.state.name, statistics=self.calculate_session_statistics())
|
||||
self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,True, self.state.name, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
|
||||
|
||||
print(self.state_manager.p2p_connection_sessions)
|
||||
print(self.arq_data_type_handler.state_manager.p2p_connection_sessions)
|
||||
|
||||
self.arq_data_type_handler.transmitted(self.type_byte, self.data, self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
|
||||
self.state_manager.remove_arq_iss_session(self.id)
|
||||
self.states.setARQ(False)
|
||||
self.arq_data_type_handler.transmitted(self.type_byte, self.data)
|
||||
return None, None
|
||||
|
||||
def transmission_failed(self, irs_frame=None):
|
||||
# final function for failed transmissions
|
||||
self.session_ended = time.time()
|
||||
self.set_state(ISS_State.FAILED)
|
||||
self.log(f"Transmission failed!")
|
||||
self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,False, self.state.name, statistics=self.calculate_session_statistics())
|
||||
self.log("Transmission failed!")
|
||||
self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,False, self.state.name, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
|
||||
self.states.setARQ(False)
|
||||
|
||||
self.arq_data_type_handler.failed(self.type_byte, self.data)
|
||||
self.arq_data_type_handler.failed(self.type_byte, self.data, self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
|
||||
return None, None
|
||||
|
||||
def abort_transmission(self, irs_frame=None):
|
||||
# function for starting the abort sequence
|
||||
self.log(f"aborting transmission...")
|
||||
self.log("aborting transmission...")
|
||||
self.set_state(ISS_State.ABORTING)
|
||||
|
||||
self.event_manager.send_arq_session_finished(
|
||||
True, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics())
|
||||
True, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
|
||||
|
||||
# break actual retries
|
||||
self.event_frame_received.set()
|
||||
|
@ -210,7 +237,7 @@ class ARQSessionISS(arq_session.ARQSession):
|
|||
self.event_frame_received.set()
|
||||
|
||||
self.event_manager.send_arq_session_finished(
|
||||
True, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics())
|
||||
True, self.id, self.dxcall, False, self.state.name, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
|
||||
self.state_manager.remove_arq_iss_session(self.id)
|
||||
self.states.setARQ(False)
|
||||
return None, None
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
"""
|
||||
Gather information about audio devices.
|
||||
"""
|
||||
import atexit
|
||||
import multiprocessing
|
||||
import crcengine
|
||||
import sounddevice as sd
|
||||
import structlog
|
||||
import numpy as np
|
||||
import queue
|
||||
import threading
|
||||
|
||||
atexit.register(sd._terminate)
|
||||
|
||||
log = structlog.get_logger("audio")
|
||||
|
||||
|
|
|
@ -74,23 +74,20 @@ def freedv_get_mode_name_by_value(mode: int) -> str:
|
|||
return FREEDV_MODE(mode).name
|
||||
|
||||
|
||||
# Check if we are running in a pyinstaller environment
|
||||
#if hasattr(sys, "_MEIPASS"):
|
||||
# sys.path.append(getattr(sys, "_MEIPASS"))
|
||||
#else:
|
||||
sys.path.append(os.path.abspath("."))
|
||||
|
||||
#log.info("[C2 ] Searching for libcodec2...")
|
||||
# Get the directory of the current script file
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append(script_dir)
|
||||
# Use script_dir to construct the paths for file search
|
||||
if sys.platform == "linux":
|
||||
files = glob.glob(r"**/*libcodec2*", recursive=True)
|
||||
files.append("libcodec2.so")
|
||||
files = glob.glob(os.path.join(script_dir, "**/*libcodec2*"), recursive=True)
|
||||
files.append(os.path.join(script_dir, "libcodec2.so"))
|
||||
elif sys.platform == "darwin":
|
||||
if hasattr(sys, "_MEIPASS"):
|
||||
files = glob.glob(getattr(sys, "_MEIPASS") + '/**/*libcodec2*', recursive=True)
|
||||
files = glob.glob(os.path.join(getattr(sys, "_MEIPASS"), '**/*libcodec2*'), recursive=True)
|
||||
else:
|
||||
files = glob.glob(r"**/*libcodec2*.dylib", recursive=True)
|
||||
files = glob.glob(os.path.join(script_dir, "**/*libcodec2*.dylib"), recursive=True)
|
||||
elif sys.platform in ["win32", "win64"]:
|
||||
files = glob.glob(r"**\*libcodec2*.dll", recursive=True)
|
||||
files = glob.glob(os.path.join(script_dir, "**\\*libcodec2*.dll"), recursive=True)
|
||||
else:
|
||||
files = []
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ from arq_data_type_handler import ARQDataTypeHandler
|
|||
|
||||
class TxCommand():
|
||||
|
||||
def __init__(self, config: dict, state_manager: StateManager, event_manager, apiParams:dict = {}):
|
||||
def __init__(self, config: dict, state_manager: StateManager, event_manager, apiParams:dict = {}, socket_command_handler=None):
|
||||
self.config = config
|
||||
self.logger = structlog.get_logger(type(self).__name__)
|
||||
self.state_manager = state_manager
|
||||
|
@ -16,6 +16,7 @@ class TxCommand():
|
|||
self.set_params_from_api(apiParams)
|
||||
self.frame_factory = DataFrameFactory(config)
|
||||
self.arq_data_type_handler = ARQDataTypeHandler(event_manager, state_manager)
|
||||
self.socket_command_handler = socket_command_handler
|
||||
|
||||
def log(self, message, isWarning = False):
|
||||
msg = f"[{type(self).__name__}]: {message}"
|
||||
|
|
|
@ -15,7 +15,7 @@ class SendMessageCommand(TxCommand):
|
|||
def set_params_from_api(self, apiParams):
|
||||
origin = f"{self.config['STATION']['mycall']}-{self.config['STATION']['myssid']}"
|
||||
self.message = MessageP2P.from_api_params(origin, apiParams)
|
||||
DatabaseManagerMessages(self.event_manager).add_message(self.message.to_dict(), direction='transmit', status='queued')
|
||||
DatabaseManagerMessages(self.event_manager).add_message(self.message.to_dict(), statistics={}, direction='transmit', status='queued')
|
||||
|
||||
def transmit(self, modem):
|
||||
|
||||
|
@ -23,6 +23,11 @@ class SendMessageCommand(TxCommand):
|
|||
self.log("Modem busy, waiting until ready...")
|
||||
return
|
||||
|
||||
if not modem:
|
||||
self.log("Modem not running...", isWarning=True)
|
||||
return
|
||||
|
||||
|
||||
first_queued_message = DatabaseManagerMessages(self.event_manager).get_first_queued_message()
|
||||
if not first_queued_message:
|
||||
self.log("No queued message in database.")
|
||||
|
|
37
modem/command_p2p_connection.py
Normal file
37
modem/command_p2p_connection.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
import queue
|
||||
from command import TxCommand
|
||||
import api_validations
|
||||
import base64
|
||||
from queue import Queue
|
||||
from p2p_connection import P2PConnection
|
||||
|
||||
class P2PConnectionCommand(TxCommand):
|
||||
|
||||
def set_params_from_api(self, apiParams):
|
||||
self.origin = apiParams['origin']
|
||||
if not api_validations.validate_freedata_callsign(self.origin):
|
||||
self.origin = f"{self.origin}-0"
|
||||
|
||||
self.destination = apiParams['destination']
|
||||
if not api_validations.validate_freedata_callsign(self.destination):
|
||||
self.destination = f"{self.destination}-0"
|
||||
|
||||
|
||||
def connect(self, event_queue: Queue, modem):
|
||||
pass
|
||||
|
||||
def run(self, event_queue: Queue, modem):
|
||||
try:
|
||||
self.emit_event(event_queue)
|
||||
session = P2PConnection(self.config, modem, self.origin, self.destination, self.state_manager, self.event_manager, self.socket_command_handler)
|
||||
print(session)
|
||||
if session.session_id:
|
||||
self.state_manager.register_p2p_connection_session(session)
|
||||
session.connect()
|
||||
return session
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"Error starting P2P Connection session: {e}", isWarning=True)
|
||||
|
||||
return False
|
|
@ -1,9 +1,10 @@
|
|||
[NETWORK]
|
||||
modemaddress = 127.0.0.1
|
||||
modemport = 5000
|
||||
|
||||
[STATION]
|
||||
mycall = AA1AAA
|
||||
mygrid = AA12aa
|
||||
mygrid = JN48ea
|
||||
myssid = 1
|
||||
ssid_list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
enable_explorer = True
|
||||
|
@ -14,7 +15,6 @@ input_device = 5a1c
|
|||
output_device = bd6c
|
||||
rx_audio_level = 0
|
||||
tx_audio_level = 0
|
||||
enable_auto_tune = False
|
||||
|
||||
[RIGCTLD]
|
||||
ip = 127.0.0.1
|
||||
|
@ -45,15 +45,17 @@ enable_protocol = False
|
|||
|
||||
[MODEM]
|
||||
enable_hmac = False
|
||||
tuning_range_fmax = 50
|
||||
tuning_range_fmin = -50
|
||||
enable_fsk = False
|
||||
enable_low_bandwidth_mode = False
|
||||
enable_morse_identifier = False
|
||||
respond_to_cq = True
|
||||
rx_buffer_size = 64
|
||||
tx_delay = 200
|
||||
beacon_interval = 300
|
||||
tx_delay = 50
|
||||
maximum_bandwidth = 1700
|
||||
enable_socket_interface = False
|
||||
|
||||
[SOCKET_INTERFACE]
|
||||
enable = False
|
||||
host = 127.0.0.1
|
||||
cmd_port = 8000
|
||||
data_port = 8001
|
||||
|
||||
[MESSAGES]
|
||||
enable_auto_repeat = False
|
||||
|
|
|
@ -10,6 +10,7 @@ class CONFIG:
|
|||
|
||||
config_types = {
|
||||
'NETWORK': {
|
||||
'modemaddress': str,
|
||||
'modemport': int,
|
||||
},
|
||||
'STATION': {
|
||||
|
@ -25,7 +26,6 @@ class CONFIG:
|
|||
'output_device': str,
|
||||
'rx_audio_level': int,
|
||||
'tx_audio_level': int,
|
||||
'enable_auto_tune': bool,
|
||||
},
|
||||
'RADIO': {
|
||||
'control': str,
|
||||
|
@ -55,16 +55,19 @@ class CONFIG:
|
|||
'enable_protocol': bool,
|
||||
},
|
||||
'MODEM': {
|
||||
'tuning_range_fmax': int,
|
||||
'tuning_range_fmin': int,
|
||||
'enable_fsk': bool,
|
||||
'enable_hmac': bool,
|
||||
'enable_morse_identifier': bool,
|
||||
'enable_low_bandwidth_mode': bool,
|
||||
'maximum_bandwidth': int,
|
||||
'respond_to_cq': bool,
|
||||
'rx_buffer_size': int,
|
||||
'tx_delay': int,
|
||||
'beacon_interval': int,
|
||||
'enable_socket_interface': bool,
|
||||
},
|
||||
'SOCKET_INTERFACE': {
|
||||
'enable' : bool,
|
||||
'host' : str,
|
||||
'cmd_port' : int,
|
||||
'data_port' : int,
|
||||
|
||||
},
|
||||
'MESSAGES': {
|
||||
'enable_auto_repeat': bool,
|
||||
|
|
|
@ -18,6 +18,7 @@ class DataFrameFactory:
|
|||
}
|
||||
|
||||
def __init__(self, config):
|
||||
|
||||
self.myfullcall = f"{config['STATION']['mycall']}-{config['STATION']['myssid']}"
|
||||
self.mygrid = config['STATION']['mygrid']
|
||||
|
||||
|
@ -28,6 +29,7 @@ class DataFrameFactory:
|
|||
self._load_ping_templates()
|
||||
self._load_fec_templates()
|
||||
self._load_arq_templates()
|
||||
self._load_p2p_connection_templates()
|
||||
|
||||
def _load_broadcast_templates(self):
|
||||
# cq frame
|
||||
|
@ -98,6 +100,7 @@ class DataFrameFactory:
|
|||
"destination_crc": 3,
|
||||
"origin": 6,
|
||||
"session_id": 1,
|
||||
"maximum_bandwidth": 2,
|
||||
}
|
||||
|
||||
self.template_list[FR_TYPE.ARQ_SESSION_OPEN_ACK.value] = {
|
||||
|
@ -144,6 +147,7 @@ class DataFrameFactory:
|
|||
self.template_list[FR_TYPE.ARQ_BURST_FRAME.value] = {
|
||||
"frame_length": None,
|
||||
"session_id": 1,
|
||||
"speed_level": 1,
|
||||
"offset": 4,
|
||||
"data": "dynamic",
|
||||
}
|
||||
|
@ -158,6 +162,63 @@ class DataFrameFactory:
|
|||
"snr": 1,
|
||||
"flag": 1,
|
||||
}
|
||||
|
||||
def _load_p2p_connection_templates(self):
|
||||
# p2p connect request
|
||||
self.template_list[FR_TYPE.P2P_CONNECTION_CONNECT.value] = {
|
||||
"frame_length": self.LENGTH_SIG1_FRAME,
|
||||
"destination_crc": 3,
|
||||
"origin": 6,
|
||||
"session_id": 1,
|
||||
}
|
||||
|
||||
# connect ACK
|
||||
self.template_list[FR_TYPE.P2P_CONNECTION_CONNECT_ACK.value] = {
|
||||
"frame_length": self.LENGTH_SIG1_FRAME,
|
||||
"destination_crc": 3,
|
||||
"origin": 6,
|
||||
"session_id": 1,
|
||||
}
|
||||
|
||||
# heartbeat for "is alive"
|
||||
self.template_list[FR_TYPE.P2P_CONNECTION_HEARTBEAT.value] = {
|
||||
"frame_length": self.LENGTH_SIG1_FRAME,
|
||||
"session_id": 1,
|
||||
}
|
||||
|
||||
# ack heartbeat
|
||||
self.template_list[FR_TYPE.P2P_CONNECTION_HEARTBEAT_ACK.value] = {
|
||||
"frame_length": self.LENGTH_SIG1_FRAME,
|
||||
"session_id": 1,
|
||||
}
|
||||
|
||||
# p2p payload frames
|
||||
self.template_list[FR_TYPE.P2P_CONNECTION_PAYLOAD.value] = {
|
||||
"frame_length": None,
|
||||
"session_id": 1,
|
||||
"sequence_id": 1,
|
||||
"data": "dynamic",
|
||||
}
|
||||
|
||||
# p2p payload frame ack
|
||||
self.template_list[FR_TYPE.P2P_CONNECTION_PAYLOAD_ACK.value] = {
|
||||
"frame_length": self.LENGTH_SIG1_FRAME,
|
||||
"session_id": 1,
|
||||
"sequence_id": 1,
|
||||
}
|
||||
|
||||
# heartbeat for "is alive"
|
||||
self.template_list[FR_TYPE.P2P_CONNECTION_DISCONNECT.value] = {
|
||||
"frame_length": self.LENGTH_SIG1_FRAME,
|
||||
"session_id": 1,
|
||||
}
|
||||
|
||||
# ack heartbeat
|
||||
self.template_list[FR_TYPE.P2P_CONNECTION_DISCONNECT_ACK.value] = {
|
||||
"frame_length": self.LENGTH_SIG1_FRAME,
|
||||
"session_id": 1,
|
||||
}
|
||||
|
||||
|
||||
|
||||
def construct(self, frametype, content, frame_length = LENGTH_SIG1_FRAME):
|
||||
|
@ -218,7 +279,7 @@ class DataFrameFactory:
|
|||
|
||||
elif key in ["session_id", "speed_level",
|
||||
"frames_per_burst", "version",
|
||||
"offset", "total_length", "state", "type"]:
|
||||
"offset", "total_length", "state", "type", "maximum_bandwidth"]:
|
||||
extracted_data[key] = int.from_bytes(data, 'big')
|
||||
|
||||
elif key in ["snr"]:
|
||||
|
@ -327,11 +388,12 @@ class DataFrameFactory:
|
|||
test_frame[:1] = bytes([FR_TYPE.TEST_FRAME.value])
|
||||
return test_frame
|
||||
|
||||
def build_arq_session_open(self, destination, session_id):
|
||||
def build_arq_session_open(self, destination, session_id, maximum_bandwidth):
|
||||
payload = {
|
||||
"destination_crc": helpers.get_crc_24(destination),
|
||||
"origin": helpers.callsign_to_bytes(self.myfullcall),
|
||||
"session_id": session_id.to_bytes(1, 'big'),
|
||||
"maximum_bandwidth": maximum_bandwidth.to_bytes(2, 'big'),
|
||||
}
|
||||
return self.construct(FR_TYPE.ARQ_SESSION_OPEN, payload)
|
||||
|
||||
|
@ -394,14 +456,16 @@ class DataFrameFactory:
|
|||
}
|
||||
return self.construct(FR_TYPE.ARQ_SESSION_INFO_ACK, payload)
|
||||
|
||||
def build_arq_burst_frame(self, freedv_mode: codec2.FREEDV_MODE, session_id: int, offset: int, data: bytes):
|
||||
def build_arq_burst_frame(self, freedv_mode: codec2.FREEDV_MODE, session_id: int, offset: int, data: bytes, speed_level: int):
|
||||
payload = {
|
||||
"session_id": session_id.to_bytes(1, 'big'),
|
||||
"speed_level": speed_level.to_bytes(1, 'big'),
|
||||
"offset": offset.to_bytes(4, 'big'),
|
||||
"data": data,
|
||||
}
|
||||
frame = self.construct(FR_TYPE.ARQ_BURST_FRAME, payload, self.get_bytes_per_frame(freedv_mode))
|
||||
return frame
|
||||
return self.construct(
|
||||
FR_TYPE.ARQ_BURST_FRAME, payload, self.get_bytes_per_frame(freedv_mode)
|
||||
)
|
||||
|
||||
def build_arq_burst_ack(self, session_id: bytes, offset, speed_level: int,
|
||||
frames_per_burst: int, snr: int, flag_final=False, flag_checksum=False, flag_abort=False):
|
||||
|
@ -415,7 +479,6 @@ class DataFrameFactory:
|
|||
if flag_abort:
|
||||
flag = helpers.set_flag(flag, 'ABORT', True, self.ARQ_FLAGS)
|
||||
|
||||
|
||||
payload = {
|
||||
"session_id": session_id.to_bytes(1, 'big'),
|
||||
"offset": offset.to_bytes(4, 'big'),
|
||||
|
@ -425,3 +488,62 @@ class DataFrameFactory:
|
|||
"flag": flag.to_bytes(1, 'big'),
|
||||
}
|
||||
return self.construct(FR_TYPE.ARQ_BURST_ACK, payload)
|
||||
|
||||
def build_p2p_connection_connect(self, destination, origin, session_id):
|
||||
payload = {
|
||||
"destination_crc": helpers.get_crc_24(destination),
|
||||
"origin": helpers.callsign_to_bytes(origin),
|
||||
"session_id": session_id.to_bytes(1, 'big'),
|
||||
}
|
||||
return self.construct(FR_TYPE.P2P_CONNECTION_CONNECT, payload)
|
||||
|
||||
def build_p2p_connection_connect_ack(self, destination, origin, session_id):
|
||||
payload = {
|
||||
"destination_crc": helpers.get_crc_24(destination),
|
||||
"origin": helpers.callsign_to_bytes(origin),
|
||||
"session_id": session_id.to_bytes(1, 'big'),
|
||||
}
|
||||
return self.construct(FR_TYPE.P2P_CONNECTION_CONNECT_ACK, payload)
|
||||
|
||||
def build_p2p_connection_heartbeat(self, session_id):
|
||||
payload = {
|
||||
"session_id": session_id.to_bytes(1, 'big'),
|
||||
}
|
||||
return self.construct(FR_TYPE.P2P_CONNECTION_HEARTBEAT, payload)
|
||||
|
||||
def build_p2p_connection_heartbeat_ack(self, session_id):
|
||||
payload = {
|
||||
"session_id": session_id.to_bytes(1, 'big'),
|
||||
}
|
||||
return self.construct(FR_TYPE.P2P_CONNECTION_HEARTBEAT_ACK, payload)
|
||||
|
||||
def build_p2p_connection_payload(self, freedv_mode: codec2.FREEDV_MODE, session_id: int, sequence_id: int, data: bytes):
|
||||
payload = {
|
||||
"session_id": session_id.to_bytes(1, 'big'),
|
||||
"sequence_id": sequence_id.to_bytes(1, 'big'),
|
||||
"data": data,
|
||||
}
|
||||
return self.construct(
|
||||
FR_TYPE.P2P_CONNECTION_PAYLOAD,
|
||||
payload,
|
||||
self.get_bytes_per_frame(freedv_mode),
|
||||
)
|
||||
|
||||
def build_p2p_connection_payload_ack(self, session_id, sequence_id):
|
||||
payload = {
|
||||
"session_id": session_id.to_bytes(1, 'big'),
|
||||
"sequence_id": sequence_id.to_bytes(1, 'big'),
|
||||
}
|
||||
return self.construct(FR_TYPE.P2P_CONNECTION_PAYLOAD_ACK, payload)
|
||||
|
||||
def build_p2p_connection_disconnect(self, session_id):
|
||||
payload = {
|
||||
"session_id": session_id.to_bytes(1, 'big'),
|
||||
}
|
||||
return self.construct(FR_TYPE.P2P_CONNECTION_DISCONNECT, payload)
|
||||
|
||||
def build_p2p_connection_disconnect_ack(self, session_id):
|
||||
payload = {
|
||||
"session_id": session_id.to_bytes(1, 'big'),
|
||||
}
|
||||
return self.construct(FR_TYPE.P2P_CONNECTION_DISCONNECT_ACK, payload)
|
||||
|
|
|
@ -4,8 +4,6 @@ import ctypes
|
|||
import structlog
|
||||
import threading
|
||||
import audio
|
||||
import os
|
||||
from modem_frametypes import FRAME_TYPE
|
||||
import itertools
|
||||
|
||||
TESTMODE = False
|
||||
|
@ -27,20 +25,16 @@ class Demodulator():
|
|||
'decoding_thread': None
|
||||
}
|
||||
|
||||
def __init__(self, config, audio_rx_q, modem_rx_q, data_q_rx, states, event_manager, fft_queue):
|
||||
def __init__(self, config, audio_rx_q, data_q_rx, states, event_manager, service_queue, fft_queue):
|
||||
self.log = structlog.get_logger("Demodulator")
|
||||
|
||||
self.tuning_range_fmin = config['MODEM']['tuning_range_fmin']
|
||||
self.tuning_range_fmax = config['MODEM']['tuning_range_fmax']
|
||||
self.rx_audio_level = config['AUDIO']['rx_audio_level']
|
||||
|
||||
self.service_queue = service_queue
|
||||
self.AUDIO_FRAMES_PER_BUFFER_RX = 4800
|
||||
self.buffer_overflow_counter = [0, 0, 0, 0, 0, 0, 0, 0]
|
||||
self.is_codec2_traffic_counter = 0
|
||||
self.is_codec2_traffic_cooldown = 5
|
||||
|
||||
self.audio_received_queue = audio_rx_q
|
||||
self.modem_received_queue = modem_rx_q
|
||||
self.data_queue_received = data_q_rx
|
||||
|
||||
self.states = states
|
||||
|
@ -79,13 +73,6 @@ class Demodulator():
|
|||
codec2.api.freedv_open(mode), ctypes.c_void_p
|
||||
)
|
||||
|
||||
# set tuning range
|
||||
codec2.api.freedv_set_tuning_range(
|
||||
c2instance,
|
||||
ctypes.c_float(float(self.tuning_range_fmin)),
|
||||
ctypes.c_float(float(self.tuning_range_fmax)),
|
||||
)
|
||||
|
||||
# get bytes per frame
|
||||
bytes_per_frame = int(
|
||||
codec2.api.freedv_get_bits_per_modem_frame(c2instance) / 8
|
||||
|
@ -135,49 +122,6 @@ class Demodulator():
|
|||
)
|
||||
self.MODE_DICT[mode]['decoding_thread'].start()
|
||||
|
||||
def sd_input_audio_callback(self, indata: np.ndarray, frames: int, time, status) -> None:
|
||||
audio_48k = np.frombuffer(indata, dtype=np.int16)
|
||||
audio_8k = self.resampler.resample48_to_8(audio_48k)
|
||||
|
||||
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)
|
||||
# Avoid buffer overflow by filling only if buffer for
|
||||
# selected datachannel mode is not full
|
||||
index = 0
|
||||
for mode in self.MODE_DICT:
|
||||
mode_data = self.MODE_DICT[mode]
|
||||
audiobuffer = mode_data['audio_buffer']
|
||||
decode = mode_data['decode']
|
||||
index += 1
|
||||
if audiobuffer:
|
||||
if (audiobuffer.nbuffer + length_audio_8k_level_adjusted) > audiobuffer.size:
|
||||
self.buffer_overflow_counter[index] += 1
|
||||
self.event_manager.send_buffer_overflow(self.buffer_overflow_counter)
|
||||
elif decode:
|
||||
audiobuffer.push(audio_8k_level_adjusted)
|
||||
|
||||
def worker_received(self) -> None:
|
||||
"""Worker for FIFO queue for processing received frames"""
|
||||
while True:
|
||||
data = self.modem_received_queue.get()
|
||||
self.log.debug("[MDM] worker_received: received data!")
|
||||
# data[0] = bytes_out
|
||||
# data[1] = freedv session
|
||||
# data[2] = bytes_per_frame
|
||||
# data[3] = snr
|
||||
|
||||
item = {
|
||||
'payload': data[0],
|
||||
'freedv': data[1],
|
||||
'bytes_per_frame': data[2],
|
||||
'snr': data[3],
|
||||
'frequency_offset': self.get_frequency_offset(data[1]),
|
||||
}
|
||||
|
||||
self.data_queue_received.put(item)
|
||||
self.modem_received_queue.task_done()
|
||||
|
||||
def get_frequency_offset(self, freedv: ctypes.c_void_p) -> float:
|
||||
"""
|
||||
|
@ -247,7 +191,16 @@ class Demodulator():
|
|||
snr = self.calculate_snr(freedv)
|
||||
self.get_scatter(freedv)
|
||||
|
||||
self.modem_received_queue.put([bytes_out, freedv, bytes_per_frame, snr])
|
||||
item = {
|
||||
'payload': bytes_out,
|
||||
'freedv': freedv,
|
||||
'bytes_per_frame': bytes_per_frame,
|
||||
'snr': snr,
|
||||
'frequency_offset': self.get_frequency_offset(freedv),
|
||||
}
|
||||
self.data_queue_received.put(item)
|
||||
|
||||
|
||||
state_buffer = []
|
||||
except Exception as e:
|
||||
error_message = str(e)
|
||||
|
@ -257,6 +210,7 @@ class Demodulator():
|
|||
self.log.debug(
|
||||
"[MDM] [demod_audio] demod loop ended", mode=mode_name, e=e
|
||||
)
|
||||
|
||||
def tci_rx_callback(self) -> None:
|
||||
"""
|
||||
Callback for TCI RX
|
||||
|
@ -297,6 +251,7 @@ class Demodulator():
|
|||
frames_per_burst = min(frames_per_burst, 1)
|
||||
frames_per_burst = max(frames_per_burst, 5)
|
||||
|
||||
# FIXME
|
||||
frames_per_burst = 1
|
||||
|
||||
codec2.api.freedv_set_frames_per_burst(self.dat0_datac1_freedv, frames_per_burst)
|
||||
|
@ -382,15 +337,19 @@ class Demodulator():
|
|||
for mode in self.MODE_DICT:
|
||||
codec2.api.freedv_set_sync(self.MODE_DICT[mode]["instance"], 0)
|
||||
|
||||
def set_decode_mode(self, mode):
|
||||
|
||||
for m in self.MODE_DICT: self.MODE_DICT[m]["decode"] = False
|
||||
def set_decode_mode(self, modes_to_decode=None):
|
||||
# Reset all modes to not decode
|
||||
for m in self.MODE_DICT:
|
||||
self.MODE_DICT[m]["decode"] = False
|
||||
|
||||
# signalling is always true
|
||||
self.MODE_DICT[codec2.FREEDV_MODE.signalling.value]["decode"] = True
|
||||
|
||||
# Enable mode based on speed_level
|
||||
self.MODE_DICT[mode.value]["decode"] = True
|
||||
self.log.info(f"[MDM] [demod_audio] set data mode: {mode.name}")
|
||||
# lowest speed level is alwys true
|
||||
self.MODE_DICT[codec2.FREEDV_MODE.datac4.value]["decode"] = True
|
||||
|
||||
return
|
||||
# Enable specified modes
|
||||
if modes_to_decode:
|
||||
for mode, decode in modes_to_decode.items():
|
||||
if mode in self.MODE_DICT:
|
||||
self.MODE_DICT[mode]["decode"] = decode
|
||||
|
|
|
@ -12,6 +12,8 @@ class EventManager:
|
|||
def broadcast(self, data):
|
||||
for q in self.queues:
|
||||
self.logger.debug(f"Event: ", ev=data)
|
||||
if q.qsize() > 10:
|
||||
q.queue.clear()
|
||||
q.put(data)
|
||||
|
||||
def send_ptt_change(self, on:bool = False):
|
||||
|
@ -42,7 +44,10 @@ class EventManager:
|
|||
}
|
||||
self.broadcast(event)
|
||||
|
||||
def send_arq_session_progress(self, outbound: bool, session_id, dxcall, received_bytes, total_bytes, state):
|
||||
def send_arq_session_progress(self, outbound: bool, session_id, dxcall, received_bytes, total_bytes, state, statistics=None):
|
||||
if statistics is None:
|
||||
statistics = {}
|
||||
|
||||
direction = 'outbound' if outbound else 'inbound'
|
||||
event = {
|
||||
"type": "arq",
|
||||
|
@ -52,6 +57,7 @@ class EventManager:
|
|||
'received_bytes': received_bytes,
|
||||
'total_bytes': total_bytes,
|
||||
'state': state,
|
||||
'statistics': statistics,
|
||||
}
|
||||
}
|
||||
self.broadcast(event)
|
||||
|
|
|
@ -9,7 +9,7 @@ Created on 05.11.23
|
|||
|
||||
import requests
|
||||
import threading
|
||||
import ujson as json
|
||||
import json
|
||||
import structlog
|
||||
import sched
|
||||
import time
|
||||
|
@ -33,7 +33,7 @@ class explorer():
|
|||
callsign = str(self.config['STATION']['mycall']) + "-" + str(self.config["STATION"]['myssid'])
|
||||
gridsquare = str(self.config['STATION']['mygrid'])
|
||||
version = str(self.modem_version)
|
||||
bandwidth = str(self.config['MODEM']['enable_low_bandwidth_mode'])
|
||||
bandwidth = str(self.config['MODEM']['maximum_bandwidth'])
|
||||
beacon = str(self.states.is_beacon_running)
|
||||
strength = str(self.states.s_meter_strength)
|
||||
|
||||
|
|
|
@ -13,8 +13,11 @@ from frame_handler import FrameHandler
|
|||
from frame_handler_ping import PingFrameHandler
|
||||
from frame_handler_cq import CQFrameHandler
|
||||
from frame_handler_arq_session import ARQFrameHandler
|
||||
from frame_handler_p2p_connection import P2PConnectionFrameHandler
|
||||
from frame_handler_beacon import BeaconFrameHandler
|
||||
|
||||
|
||||
|
||||
class DISPATCHER():
|
||||
|
||||
FRAME_HANDLER = {
|
||||
|
@ -22,9 +25,18 @@ class DISPATCHER():
|
|||
FR_TYPE.ARQ_SESSION_OPEN.value: {"class": ARQFrameHandler, "name": "ARQ Data Channel Open"},
|
||||
FR_TYPE.ARQ_SESSION_INFO_ACK.value: {"class": ARQFrameHandler, "name": "ARQ INFO ACK"},
|
||||
FR_TYPE.ARQ_SESSION_INFO.value: {"class": ARQFrameHandler, "name": "ARQ Data Channel Info"},
|
||||
FR_TYPE.ARQ_CONNECTION_CLOSE.value: {"class": ARQFrameHandler, "name": "ARQ CLOSE SESSION"},
|
||||
FR_TYPE.ARQ_CONNECTION_HB.value: {"class": ARQFrameHandler, "name": "ARQ HEARTBEAT"},
|
||||
FR_TYPE.ARQ_CONNECTION_OPEN.value: {"class": ARQFrameHandler, "name": "ARQ OPEN SESSION"},
|
||||
FR_TYPE.P2P_CONNECTION_CONNECT.value: {"class": P2PConnectionFrameHandler, "name": "P2P Connection CONNECT"},
|
||||
FR_TYPE.P2P_CONNECTION_CONNECT_ACK.value: {"class": P2PConnectionFrameHandler, "name": "P2P Connection CONNECT ACK"},
|
||||
FR_TYPE.P2P_CONNECTION_DISCONNECT.value: {"class": P2PConnectionFrameHandler, "name": "P2P Connection DISCONNECT"},
|
||||
FR_TYPE.P2P_CONNECTION_DISCONNECT_ACK.value: {"class": P2PConnectionFrameHandler,
|
||||
"name": "P2P Connection DISCONNECT ACK"},
|
||||
FR_TYPE.P2P_CONNECTION_PAYLOAD.value: {"class": P2PConnectionFrameHandler,
|
||||
"name": "P2P Connection PAYLOAD"},
|
||||
FR_TYPE.P2P_CONNECTION_PAYLOAD_ACK.value: {"class": P2PConnectionFrameHandler,
|
||||
"name": "P2P Connection PAYLOAD ACK"},
|
||||
|
||||
#FR_TYPE.ARQ_CONNECTION_HB.value: {"class": ARQFrameHandler, "name": "ARQ HEARTBEAT"},
|
||||
#FR_TYPE.ARQ_CONNECTION_OPEN.value: {"class": ARQFrameHandler, "name": "ARQ OPEN SESSION"},
|
||||
FR_TYPE.ARQ_STOP.value: {"class": ARQFrameHandler, "name": "ARQ STOP"},
|
||||
FR_TYPE.ARQ_STOP_ACK.value: {"class": ARQFrameHandler, "name": "ARQ STOP ACK"},
|
||||
FR_TYPE.BEACON.value: {"class": BeaconFrameHandler, "name": "BEACON"},
|
||||
|
@ -82,7 +94,7 @@ class DISPATCHER():
|
|||
|
||||
if frametype not in self.FRAME_HANDLER:
|
||||
self.log.warning(
|
||||
"[Modem] ARQ - other frame type", frametype=FR_TYPE(frametype).name)
|
||||
"[DISPATCHER] ARQ - other frame type", frametype=FR_TYPE(frametype).name)
|
||||
return
|
||||
|
||||
# instantiate handler
|
||||
|
|
|
@ -6,9 +6,11 @@ import structlog
|
|||
import time, uuid
|
||||
from codec2 import FREEDV_MODE
|
||||
from message_system_db_manager import DatabaseManager
|
||||
import maidenhead
|
||||
|
||||
TESTMODE = False
|
||||
|
||||
|
||||
class FrameHandler():
|
||||
|
||||
def __init__(self, name: str, config, states: StateManager, event_manager: EventManager,
|
||||
|
@ -34,7 +36,7 @@ class FrameHandler():
|
|||
ft = self.details['frame']['frame_type']
|
||||
valid = False
|
||||
# Check for callsign checksum
|
||||
if ft in ['ARQ_SESSION_OPEN', 'ARQ_SESSION_OPEN_ACK', 'PING', 'PING_ACK']:
|
||||
if ft in ['ARQ_SESSION_OPEN', 'ARQ_SESSION_OPEN_ACK', 'PING', 'PING_ACK', 'P2P_CONNECTION_CONNECT']:
|
||||
valid, mycallsign = helpers.check_callsign(
|
||||
call_with_ssid,
|
||||
self.details["frame"]["destination_crc"],
|
||||
|
@ -51,6 +53,20 @@ class FrameHandler():
|
|||
session_id = self.details['frame']['session_id']
|
||||
if session_id in self.states.arq_iss_sessions:
|
||||
valid = True
|
||||
|
||||
# check for p2p connection
|
||||
elif ft in ['P2P_CONNECTION_CONNECT']:
|
||||
valid, mycallsign = helpers.check_callsign(
|
||||
call_with_ssid,
|
||||
self.details["frame"]["destination_crc"],
|
||||
self.config['STATION']['ssid_list'])
|
||||
|
||||
#check for p2p connection
|
||||
elif ft in ['P2P_CONNECTION_CONNECT_ACK', 'P2P_CONNECTION_PAYLOAD', 'P2P_CONNECTION_PAYLOAD_ACK', 'P2P_CONNECTION_DISCONNECT', 'P2P_CONNECTION_DISCONNECT_ACK']:
|
||||
session_id = self.details['frame']['session_id']
|
||||
if session_id in self.states.p2p_connection_sessions:
|
||||
valid = True
|
||||
|
||||
else:
|
||||
valid = False
|
||||
|
||||
|
@ -87,15 +103,21 @@ class FrameHandler():
|
|||
|
||||
self.states.add_activity(activity)
|
||||
|
||||
|
||||
def add_to_heard_stations(self):
|
||||
frame = self.details['frame']
|
||||
|
||||
print(frame)
|
||||
if 'origin' not in frame:
|
||||
return
|
||||
|
||||
dxgrid = frame['gridsquare'] if 'gridsquare' in frame else "------"
|
||||
dxgrid = frame.get('gridsquare', "------")
|
||||
# Initialize distance values
|
||||
distance_km = None
|
||||
distance_miles = None
|
||||
if dxgrid != "------" and frame.get('gridsquare'):
|
||||
distance_dict = maidenhead.distance_between_locators(self.config['STATION']['mygrid'], frame['gridsquare'])
|
||||
distance_km = distance_dict['kilometers']
|
||||
distance_miles = distance_dict['miles']
|
||||
|
||||
helpers.add_to_heard_stations(
|
||||
frame['origin'],
|
||||
dxgrid,
|
||||
|
@ -104,8 +126,9 @@ class FrameHandler():
|
|||
self.details['frequency_offset'],
|
||||
self.states.radio_frequency,
|
||||
self.states.heard_stations,
|
||||
distance_km=distance_km, # Pass the kilometer distance
|
||||
distance_miles=distance_miles # Pass the miles distance
|
||||
)
|
||||
|
||||
def make_event(self):
|
||||
|
||||
event = {
|
||||
|
@ -119,6 +142,13 @@ class FrameHandler():
|
|||
if 'origin' in self.details['frame']:
|
||||
event['dxcallsign'] = self.details['frame']['origin']
|
||||
|
||||
if 'gridsquare' in self.details['frame']:
|
||||
event['gridsquare'] = self.details['frame']['gridsquare']
|
||||
|
||||
distance = maidenhead.distance_between_locators(self.config['STATION']['mygrid'], self.details['frame']['gridsquare'])
|
||||
event['distance_kilometers'] = distance['kilometers']
|
||||
event['distance_miles'] = distance['miles']
|
||||
|
||||
return event
|
||||
|
||||
def emit_event(self):
|
||||
|
|
|
@ -32,7 +32,8 @@ class ARQFrameHandler(frame_handler.FrameHandler):
|
|||
session = ARQSessionIRS(self.config,
|
||||
self.modem,
|
||||
frame['origin'],
|
||||
session_id)
|
||||
session_id,
|
||||
self.states)
|
||||
self.states.register_arq_irs_session(session)
|
||||
|
||||
elif frame['frame_type_int'] in [
|
||||
|
|
|
@ -5,11 +5,22 @@ import frame_handler
|
|||
from message_system_db_messages import DatabaseManagerMessages
|
||||
|
||||
|
||||
class CQFrameHandler(frame_handler_ping.PingFrameHandler):
|
||||
class CQFrameHandler(frame_handler.FrameHandler):
|
||||
|
||||
def should_respond(self):
|
||||
self.logger.debug(f"Respond to CQ: {self.config['MODEM']['respond_to_cq']}")
|
||||
return self.config['MODEM']['respond_to_cq']
|
||||
#def should_respond(self):
|
||||
# self.logger.debug(f"Respond to CQ: {self.config['MODEM']['respond_to_cq']}")
|
||||
# return bool(self.config['MODEM']['respond_to_cq'] and not self.states.getARQ())
|
||||
|
||||
def follow_protocol(self):
|
||||
if self.states.getARQ():
|
||||
return
|
||||
|
||||
self.logger.debug(
|
||||
f"[Modem] Responding to request from [{self.details['frame']['origin']}]",
|
||||
snr=self.details['snr'],
|
||||
)
|
||||
|
||||
self.send_ack()
|
||||
|
||||
def send_ack(self):
|
||||
factory = data_frame_factory.DataFrameFactory(self.config)
|
||||
|
|
54
modem/frame_handler_p2p_connection.py
Normal file
54
modem/frame_handler_p2p_connection.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
from queue import Queue
|
||||
import frame_handler
|
||||
from event_manager import EventManager
|
||||
from state_manager import StateManager
|
||||
from modem_frametypes import FRAME_TYPE as FR
|
||||
from p2p_connection import P2PConnection
|
||||
|
||||
class P2PConnectionFrameHandler(frame_handler.FrameHandler):
|
||||
|
||||
def follow_protocol(self):
|
||||
|
||||
if not self.should_respond():
|
||||
return
|
||||
|
||||
frame = self.details['frame']
|
||||
session_id = frame['session_id']
|
||||
snr = self.details["snr"]
|
||||
frequency_offset = self.details["frequency_offset"]
|
||||
|
||||
if frame['frame_type_int'] == FR.P2P_CONNECTION_CONNECT.value:
|
||||
|
||||
# Lost OPEN_ACK case .. ISS will retry opening a session
|
||||
if session_id in self.states.arq_irs_sessions:
|
||||
session = self.states.p2p_connection_sessions[session_id]
|
||||
|
||||
# Normal case when receiving a SESSION_OPEN for the first time
|
||||
else:
|
||||
# if self.states.check_if_running_arq_session():
|
||||
# self.logger.warning("DISCARDING SESSION OPEN because of ongoing ARQ session ", frame=frame)
|
||||
# return
|
||||
print(frame)
|
||||
session = P2PConnection(self.config,
|
||||
self.modem,
|
||||
frame['origin'],
|
||||
frame['destination_crc'],
|
||||
self.states, self.event_manager)
|
||||
session.session_id = session_id
|
||||
self.states.register_p2p_connection_session(session)
|
||||
|
||||
elif frame['frame_type_int'] in [
|
||||
FR.P2P_CONNECTION_CONNECT_ACK.value,
|
||||
FR.P2P_CONNECTION_DISCONNECT.value,
|
||||
FR.P2P_CONNECTION_DISCONNECT_ACK.value,
|
||||
FR.P2P_CONNECTION_PAYLOAD.value,
|
||||
FR.P2P_CONNECTION_PAYLOAD_ACK.value,
|
||||
]:
|
||||
session = self.states.get_p2p_connection_session(session_id)
|
||||
|
||||
else:
|
||||
self.logger.warning("DISCARDING FRAME", frame=frame)
|
||||
return
|
||||
|
||||
session.set_details(snr, frequency_offset)
|
||||
session.on_frame_received(frame)
|
|
@ -15,15 +15,11 @@ class PingFrameHandler(frame_handler.FrameHandler):
|
|||
# ft = self.details['frame']['frame_type']
|
||||
# self.logger.info(f"[Modem] {ft} received but not for us.")
|
||||
# return valid
|
||||
|
||||
#def should_respond(self):
|
||||
# return self.is_frame_for_me()
|
||||
|
||||
def follow_protocol(self):
|
||||
|
||||
if not self.should_respond():
|
||||
if not bool(self.is_frame_for_me() and not self.states.getARQ()):
|
||||
return
|
||||
|
||||
|
||||
self.logger.debug(
|
||||
f"[Modem] Responding to request from [{self.details['frame']['origin']}]",
|
||||
snr=self.details['snr'],
|
||||
|
|
100
modem/helpers.py
100
modem/helpers.py
|
@ -121,60 +121,44 @@ def get_crc_32(data: str) -> bytes:
|
|||
return crc_algorithm(data).to_bytes(4, byteorder="big")
|
||||
|
||||
|
||||
def add_to_heard_stations(dxcallsign, dxgrid, datatype, snr, offset, frequency, heard_stations_list):
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
import time
|
||||
|
||||
|
||||
def add_to_heard_stations(dxcallsign, dxgrid, datatype, snr, offset, frequency, heard_stations_list, distance_km=None,
|
||||
distance_miles=None):
|
||||
"""
|
||||
Args:
|
||||
dxcallsign:
|
||||
dxgrid:
|
||||
datatype:
|
||||
snr:
|
||||
offset:
|
||||
frequency:
|
||||
dxcallsign (str): The callsign of the DX station.
|
||||
dxgrid (str): The Maidenhead grid square of the DX station.
|
||||
datatype (str): The type of data received (e.g., FT8, CW).
|
||||
snr (int): Signal-to-noise ratio of the received signal.
|
||||
offset (float): Frequency offset.
|
||||
frequency (float): Base frequency of the received signal.
|
||||
heard_stations_list (list): List containing heard stations.
|
||||
distance_km (float): Distance to the DX station in kilometers.
|
||||
distance_miles (float): Distance to the DX station in miles.
|
||||
|
||||
Returns:
|
||||
Nothing
|
||||
Nothing. The function updates the heard_stations_list in-place.
|
||||
"""
|
||||
# check if buffer empty
|
||||
if len(heard_stations_list) == 0:
|
||||
heard_stations_list.append(
|
||||
[dxcallsign, dxgrid, int(datetime.now(timezone.utc).timestamp()), datatype, snr, offset, frequency]
|
||||
)
|
||||
# if not, we search and update
|
||||
# Convert current timestamp to an integer
|
||||
current_timestamp = int(datetime.now(timezone.utc).timestamp())
|
||||
|
||||
# Initialize the new entry
|
||||
new_entry = [
|
||||
dxcallsign, dxgrid, current_timestamp, datatype, snr, offset, frequency, distance_km, distance_miles
|
||||
]
|
||||
|
||||
# Check if the buffer is empty or if the callsign is not already in the list
|
||||
if not any(dxcallsign == station[0] for station in heard_stations_list):
|
||||
heard_stations_list.append(new_entry)
|
||||
else:
|
||||
for i in range(len(heard_stations_list)):
|
||||
# Update callsign with new timestamp
|
||||
if heard_stations_list[i].count(dxcallsign) > 0:
|
||||
heard_stations_list[i] = [
|
||||
dxcallsign,
|
||||
dxgrid,
|
||||
int(time.time()),
|
||||
datatype,
|
||||
snr,
|
||||
offset,
|
||||
frequency,
|
||||
]
|
||||
# Search for the existing entry and update
|
||||
for i, entry in enumerate(heard_stations_list):
|
||||
if entry[0] == dxcallsign:
|
||||
heard_stations_list[i] = new_entry
|
||||
break
|
||||
# Insert if nothing found
|
||||
if i == len(heard_stations_list) - 1:
|
||||
heard_stations_list.append(
|
||||
[
|
||||
dxcallsign,
|
||||
dxgrid,
|
||||
int(time.time()),
|
||||
datatype,
|
||||
snr,
|
||||
offset,
|
||||
frequency,
|
||||
]
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
# for idx, item in enumerate(heard_stations_list):
|
||||
# if dxcallsign in item:
|
||||
# item = [dxcallsign, int(time.time())]
|
||||
# heard_stations_list[idx] = item
|
||||
|
||||
|
||||
def callsign_to_bytes(callsign: str) -> bytes:
|
||||
|
@ -713,15 +697,16 @@ def get_flag(byte, flag_name, flag_dict):
|
|||
return get_bit(byte, position)
|
||||
|
||||
|
||||
def find_binary_path(binary_name="rigctld", search_system_wide=False):
|
||||
def find_binary_paths(binary_name="rigctld", search_system_wide=False):
|
||||
"""
|
||||
Search for a binary within the current working directory and its subdirectories.
|
||||
Optionally, check system-wide locations and PATH environment variable if not found.
|
||||
Search for a binary within the current working directory, its subdirectories, and optionally,
|
||||
system-wide locations and the PATH environment variable.
|
||||
|
||||
:param binary_name: The base name of the binary to search for, without extension.
|
||||
:param search_system_wide: Boolean flag to enable or disable system-wide search.
|
||||
:return: The full path to the binary if found, otherwise None.
|
||||
:return: A list of full paths to the binary if found, otherwise an empty list.
|
||||
"""
|
||||
binary_paths = [] # Initialize an empty list to store found paths
|
||||
# Adjust binary name for Windows
|
||||
if platform.system() == 'Windows':
|
||||
binary_name += ".exe"
|
||||
|
@ -730,7 +715,7 @@ def find_binary_path(binary_name="rigctld", search_system_wide=False):
|
|||
root_path = os.getcwd()
|
||||
for dirpath, dirnames, filenames in os.walk(root_path):
|
||||
if binary_name in filenames:
|
||||
return os.path.join(dirpath, binary_name)
|
||||
binary_paths.append(os.path.join(dirpath, binary_name))
|
||||
|
||||
# If system-wide search is enabled, look in system locations and PATH
|
||||
if search_system_wide:
|
||||
|
@ -739,13 +724,16 @@ def find_binary_path(binary_name="rigctld", search_system_wide=False):
|
|||
if platform.system() != 'Windows':
|
||||
system_paths.extend(['/usr/bin', '/usr/local/bin', '/bin'])
|
||||
else:
|
||||
system_paths.extend(glob.glob("C:\\Program Files\\Hamlib*\\bin"))
|
||||
system_paths.extend(glob.glob("C:\\Program Files (x86)\\Hamlib*\\bin"))
|
||||
|
||||
system_paths.extend(glob.glob("C:\\Program Files\\Hamlib*\\bin"))
|
||||
system_paths.extend(glob.glob("C:\\Program Files (x86)\\Hamlib*\\bin"))
|
||||
|
||||
for path in system_paths:
|
||||
potential_path = os.path.join(path, binary_name)
|
||||
if os.path.isfile(potential_path):
|
||||
return potential_path
|
||||
binary_paths.append(potential_path)
|
||||
|
||||
return binary_paths
|
||||
|
||||
|
||||
|
||||
def kill_and_execute(binary_path, additional_args=None):
|
||||
|
|
94
modem/maidenhead.py
Normal file
94
modem/maidenhead.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
import math
|
||||
|
||||
def haversine(lat1, lon1, lat2, lon2):
|
||||
"""
|
||||
Calculate the great circle distance in kilometers between two points
|
||||
on the Earth (specified in decimal degrees).
|
||||
|
||||
Parameters:
|
||||
lat1, lon1: Latitude and longitude of point 1.
|
||||
lat2, lon2: Latitude and longitude of point 2.
|
||||
|
||||
Returns:
|
||||
float: Distance between the two points in kilometers.
|
||||
"""
|
||||
# Radius of the Earth in kilometers. Use 3956 for miles
|
||||
R = 6371.0
|
||||
|
||||
# Convert latitude and longitude from degrees to radians
|
||||
lat1 = math.radians(lat1)
|
||||
lon1 = math.radians(lon1)
|
||||
lat2 = math.radians(lat2)
|
||||
lon2 = math.radians(lon2)
|
||||
|
||||
# Difference in coordinates
|
||||
dlon = lon2 - lon1
|
||||
dlat = lat2 - lat1
|
||||
|
||||
# Haversine formula
|
||||
a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
|
||||
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
||||
|
||||
distance = R * c
|
||||
|
||||
return distance
|
||||
|
||||
|
||||
def maidenhead_to_latlon(grid_square):
|
||||
"""
|
||||
Convert a Maidenhead locator to latitude and longitude coordinates.
|
||||
The output coordinates represent the southwestern corner of the grid square.
|
||||
|
||||
Parameters:
|
||||
grid_square (str): The Maidenhead locator.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the latitude and longitude (in that order) of the grid square's center.
|
||||
"""
|
||||
if len(grid_square) < 4 or len(grid_square) % 2 != 0:
|
||||
raise ValueError("Grid square must be at least 4 characters long and an even length.")
|
||||
|
||||
grid_square = grid_square.upper()
|
||||
lon = -180 + (ord(grid_square[0]) - ord('A')) * 20
|
||||
lat = -90 + (ord(grid_square[1]) - ord('A')) * 10
|
||||
lon += (int(grid_square[2]) * 2)
|
||||
lat += int(grid_square[3])
|
||||
|
||||
if len(grid_square) >= 6:
|
||||
lon += (ord(grid_square[4]) - ord('A')) * (5 / 60)
|
||||
lat += (ord(grid_square[5]) - ord('A')) * (2.5 / 60)
|
||||
|
||||
if len(grid_square) == 8:
|
||||
lon += int(grid_square[6]) * (5 / 600)
|
||||
lat += int(grid_square[7]) * (2.5 / 600)
|
||||
|
||||
# Adjust to the center of the grid square
|
||||
if len(grid_square) <= 4:
|
||||
lon += 1
|
||||
lat += 0.5
|
||||
elif len(grid_square) == 6:
|
||||
lon += 2.5 / 60
|
||||
lat += 1.25 / 60
|
||||
else:
|
||||
lon += 2.5 / 600
|
||||
lat += 1.25 / 600
|
||||
|
||||
return lat, lon
|
||||
|
||||
|
||||
def distance_between_locators(locator1, locator2):
|
||||
"""
|
||||
Calculate the distance between two Maidenhead locators and return the result as a dictionary.
|
||||
|
||||
Parameters:
|
||||
locator1 (str): The first Maidenhead locator.
|
||||
locator2 (str): The second Maidenhead locator.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the distances in kilometers and miles.
|
||||
"""
|
||||
lat1, lon1 = maidenhead_to_latlon(locator1)
|
||||
lat2, lon2 = maidenhead_to_latlon(locator2)
|
||||
km = haversine(lat1, lon1, lat2, lon2)
|
||||
miles = km * 0.621371
|
||||
return {'kilometers': km, 'miles': miles}
|
|
@ -7,23 +7,28 @@ from message_system_db_messages import DatabaseManagerMessages
|
|||
#import command_message_send
|
||||
|
||||
|
||||
def message_received(event_manager, state_manager, data):
|
||||
def message_received(event_manager, state_manager, data, statistics):
|
||||
decompressed_json_string = data.decode('utf-8')
|
||||
received_message_obj = MessageP2P.from_payload(decompressed_json_string)
|
||||
received_message_dict = MessageP2P.to_dict(received_message_obj)
|
||||
DatabaseManagerMessages(event_manager).add_message(received_message_dict, direction='receive', status='received', is_read=False)
|
||||
DatabaseManagerMessages(event_manager).add_message(received_message_dict, statistics, direction='receive', status='received', is_read=False)
|
||||
|
||||
def message_transmitted(event_manager, state_manager, data):
|
||||
def message_transmitted(event_manager, state_manager, data, statistics):
|
||||
decompressed_json_string = data.decode('utf-8')
|
||||
payload_message_obj = MessageP2P.from_payload(decompressed_json_string)
|
||||
payload_message = MessageP2P.to_dict(payload_message_obj)
|
||||
# Todo we need to optimize this - WIP
|
||||
DatabaseManagerMessages(event_manager).update_message(payload_message["id"], update_data={'status': 'transmitted'})
|
||||
DatabaseManagerMessages(event_manager).update_message(payload_message["id"], update_data={'statistics': statistics})
|
||||
|
||||
def message_failed(event_manager, state_manager, data):
|
||||
|
||||
def message_failed(event_manager, state_manager, data, statistics):
|
||||
decompressed_json_string = data.decode('utf-8')
|
||||
payload_message_obj = MessageP2P.from_payload(decompressed_json_string)
|
||||
payload_message = MessageP2P.to_dict(payload_message_obj)
|
||||
# Todo we need to optimize this - WIP
|
||||
DatabaseManagerMessages(event_manager).update_message(payload_message["id"], update_data={'status': 'failed'})
|
||||
DatabaseManagerMessages(event_manager).update_message(payload_message["id"], update_data={'statistics': statistics})
|
||||
|
||||
class MessageP2P:
|
||||
def __init__(self, id: str, origin: str, destination: str, body: str, attachments: list) -> None:
|
||||
|
|
|
@ -2,12 +2,16 @@ from message_system_db_manager import DatabaseManager
|
|||
from message_system_db_model import MessageAttachment, Attachment, P2PMessage
|
||||
import json
|
||||
import hashlib
|
||||
|
||||
import os
|
||||
|
||||
|
||||
class DatabaseManagerAttachments(DatabaseManager):
|
||||
def __init__(self, uri='sqlite:///freedata-messages.db'):
|
||||
super().__init__(uri)
|
||||
def __init__(self, db_file=None):
|
||||
if not db_file:
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
db_path = os.path.join(script_dir, 'freedata-messages.db')
|
||||
db_file = 'sqlite:///' + db_path
|
||||
super().__init__(db_file)
|
||||
|
||||
|
||||
def add_attachment(self, session, message, attachment_data):
|
||||
|
|
|
@ -4,14 +4,16 @@ from sqlalchemy.orm import scoped_session, sessionmaker
|
|||
from threading import local
|
||||
from message_system_db_model import Base, Beacon, Station, Status, Attachment, P2PMessage
|
||||
from datetime import timezone, timedelta, datetime
|
||||
import json
|
||||
import structlog
|
||||
import helpers
|
||||
|
||||
import os
|
||||
|
||||
class DatabaseManagerBeacon(DatabaseManager):
|
||||
def __init__(self, uri):
|
||||
super().__init__(uri)
|
||||
def __init__(self, db_file=None):
|
||||
if not db_file:
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
db_path = os.path.join(script_dir, 'freedata-messages.db')
|
||||
db_file = 'sqlite:///' + db_path
|
||||
|
||||
super().__init__(db_file)
|
||||
|
||||
def add_beacon(self, timestamp, callsign, snr, gridsquare):
|
||||
session = None
|
||||
|
|
|
@ -7,12 +7,17 @@ from threading import local
|
|||
from message_system_db_model import Base, Station, Status
|
||||
import structlog
|
||||
import helpers
|
||||
import os
|
||||
|
||||
class DatabaseManager:
|
||||
def __init__(self, event_manger, uri='sqlite:///freedata-messages.db'):
|
||||
def __init__(self, event_manger, db_file=None):
|
||||
self.event_manager = event_manger
|
||||
if not db_file:
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
db_path = os.path.join(script_dir, 'freedata-messages.db')
|
||||
db_file = 'sqlite:///' + db_path
|
||||
|
||||
self.engine = create_engine(uri, echo=False)
|
||||
self.engine = create_engine(db_file, echo=False)
|
||||
self.thread_local = local()
|
||||
self.session_factory = sessionmaker(bind=self.engine)
|
||||
Base.metadata.create_all(self.engine)
|
||||
|
|
|
@ -4,14 +4,20 @@ from message_system_db_model import Status, P2PMessage
|
|||
from sqlalchemy.exc import IntegrityError
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
class DatabaseManagerMessages(DatabaseManager):
|
||||
def __init__(self, uri='sqlite:///freedata-messages.db'):
|
||||
super().__init__(uri)
|
||||
self.attachments_manager = DatabaseManagerAttachments(uri)
|
||||
def __init__(self, db_file=None):
|
||||
if not db_file:
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
db_path = os.path.join(script_dir, 'freedata-messages.db')
|
||||
db_file = 'sqlite:///' + db_path
|
||||
|
||||
def add_message(self, message_data, direction='receive', status=None, is_read=True):
|
||||
super().__init__(db_file)
|
||||
self.attachments_manager = DatabaseManagerAttachments(db_file)
|
||||
|
||||
def add_message(self, message_data, statistics, direction='receive', status=None, is_read=True):
|
||||
session = self.get_thread_scoped_session()
|
||||
try:
|
||||
# Create and add the origin and destination Stations
|
||||
|
@ -34,7 +40,8 @@ class DatabaseManagerMessages(DatabaseManager):
|
|||
direction=direction,
|
||||
status_id=status.id if status else None,
|
||||
is_read=is_read,
|
||||
attempt=0
|
||||
attempt=0,
|
||||
statistics=statistics
|
||||
)
|
||||
|
||||
session.add(new_message)
|
||||
|
@ -130,6 +137,8 @@ class DatabaseManagerMessages(DatabaseManager):
|
|||
message.body = update_data['body']
|
||||
if 'status' in update_data:
|
||||
message.status = self.get_or_create_status(session, update_data['status'])
|
||||
if 'statistics' in update_data:
|
||||
message.statistics = update_data['statistics']
|
||||
|
||||
session.commit()
|
||||
self.log(f"Updated: {message_id}")
|
||||
|
|
369
modem/modem.py
369
modem/modem.py
|
@ -9,10 +9,7 @@ Created on Wed Dec 23 07:04:24 2020
|
|||
# pylint: disable=invalid-name, line-too-long, c-extension-no-member
|
||||
# pylint: disable=import-outside-toplevel
|
||||
|
||||
import atexit
|
||||
import ctypes
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
import codec2
|
||||
import numpy as np
|
||||
|
@ -20,9 +17,9 @@ import sounddevice as sd
|
|||
import structlog
|
||||
import tci
|
||||
import cw
|
||||
from queues import RIGCTLD_COMMAND_QUEUE
|
||||
import audio
|
||||
import demodulator
|
||||
import modulator
|
||||
|
||||
TESTMODE = False
|
||||
|
||||
|
@ -44,50 +41,52 @@ class RF:
|
|||
self.audio_input_device = config['AUDIO']['input_device']
|
||||
self.audio_output_device = config['AUDIO']['output_device']
|
||||
|
||||
self.tx_audio_level = config['AUDIO']['tx_audio_level']
|
||||
self.enable_audio_auto_tune = config['AUDIO']['enable_auto_tune']
|
||||
self.tx_delay = config['MODEM']['tx_delay']
|
||||
|
||||
|
||||
self.radiocontrol = config['RADIO']['control']
|
||||
self.rigctld_ip = config['RIGCTLD']['ip']
|
||||
self.rigctld_port = config['RIGCTLD']['port']
|
||||
|
||||
self.states.setTransmitting(False)
|
||||
|
||||
self.ptt_state = False
|
||||
self.radio_alc = 0.0
|
||||
|
||||
self.tci_ip = config['TCI']['tci_ip']
|
||||
self.tci_port = config['TCI']['tci_port']
|
||||
|
||||
self.tx_audio_level = config['AUDIO']['tx_audio_level']
|
||||
self.rx_audio_level = config['AUDIO']['rx_audio_level']
|
||||
|
||||
|
||||
self.ptt_state = False
|
||||
self.enqueuing_audio = False # set to True, while we are processing audio
|
||||
|
||||
self.AUDIO_SAMPLE_RATE = 48000
|
||||
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
||||
self.modem_sample_rate = codec2.api.FREEDV_FS_8000
|
||||
|
||||
# 8192 Let's do some tests with very small chunks for TX
|
||||
self.AUDIO_FRAMES_PER_BUFFER_TX = 1200 if self.radiocontrol in ["tci"] else 2400 * 2
|
||||
# 8 * (self.AUDIO_SAMPLE_RATE/self.MODEM_SAMPLE_RATE) == 48
|
||||
#self.AUDIO_FRAMES_PER_BUFFER_TX = 1200 if self.radiocontrol in ["tci"] else 2400 * 2
|
||||
# 8 * (self.AUDIO_SAMPLE_RATE/self.modem_sample_rate) == 48
|
||||
self.AUDIO_CHANNELS = 1
|
||||
self.MODE = 0
|
||||
self.rms_counter = 0
|
||||
|
||||
# Make sure our resampler will work
|
||||
assert (self.AUDIO_SAMPLE_RATE / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 # type: ignore
|
||||
self.audio_out_queue = queue.Queue()
|
||||
|
||||
# Make sure our resampler will work
|
||||
assert (self.AUDIO_SAMPLE_RATE / self.modem_sample_rate) == codec2.api.FDMDV_OS_48 # type: ignore
|
||||
|
||||
self.modem_received_queue = queue.Queue()
|
||||
self.audio_received_queue = queue.Queue()
|
||||
self.data_queue_received = queue.Queue()
|
||||
self.fft_queue = fft_queue
|
||||
|
||||
self.demodulator = demodulator.Demodulator(self.config,
|
||||
self.audio_received_queue,
|
||||
self.modem_received_queue,
|
||||
self.data_queue_received,
|
||||
self.states,
|
||||
self.event_manager,
|
||||
self.service_queue,
|
||||
self.fft_queue
|
||||
)
|
||||
|
||||
self.modulator = modulator.Modulator(self.config)
|
||||
|
||||
|
||||
|
||||
def tci_tx_callback(self, audio_48k) -> None:
|
||||
|
@ -105,11 +104,6 @@ class RF:
|
|||
if not self.init_audio():
|
||||
raise RuntimeError("Unable to init audio devices")
|
||||
self.demodulator.start(self.sd_input_stream)
|
||||
atexit.register(self.sd_input_stream.stop)
|
||||
|
||||
# Initialize codec2, rig control, and data threads
|
||||
self.init_codec2()
|
||||
self.init_data_threads()
|
||||
|
||||
return True
|
||||
|
||||
|
@ -155,17 +149,25 @@ class RF:
|
|||
self.sd_input_stream = sd.InputStream(
|
||||
channels=1,
|
||||
dtype="int16",
|
||||
callback=self.demodulator.sd_input_audio_callback,
|
||||
callback=self.sd_input_audio_callback,
|
||||
device=in_dev_index,
|
||||
samplerate=self.AUDIO_SAMPLE_RATE,
|
||||
blocksize=4800,
|
||||
)
|
||||
self.sd_input_stream.start()
|
||||
|
||||
self.sd_output_stream = sd.OutputStream(
|
||||
channels=1,
|
||||
dtype="int16",
|
||||
callback=self.sd_output_audio_callback,
|
||||
device=out_dev_index,
|
||||
samplerate=self.AUDIO_SAMPLE_RATE,
|
||||
blocksize=4800,
|
||||
)
|
||||
self.sd_output_stream.start()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
except Exception as audioerr:
|
||||
self.log.error("[MDM] init: starting pyaudio callback failed", e=audioerr)
|
||||
self.stop_modem()
|
||||
|
@ -188,191 +190,7 @@ class RF:
|
|||
|
||||
return True
|
||||
|
||||
def audio_auto_tune(self):
|
||||
# enable / disable AUDIO TUNE Feature / ALC correction
|
||||
if self.enable_audio_auto_tune:
|
||||
if self.radio_alc == 0.0:
|
||||
self.tx_audio_level = self.tx_audio_level + 20
|
||||
elif 0.0 < self.radio_alc <= 0.1:
|
||||
print("0.0 < self.radio_alc <= 0.1")
|
||||
self.tx_audio_level = self.tx_audio_level + 2
|
||||
self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level),
|
||||
alc_level=str(self.radio_alc))
|
||||
elif 0.1 < self.radio_alc < 0.2:
|
||||
print("0.1 < self.radio_alc < 0.2")
|
||||
self.tx_audio_level = self.tx_audio_level
|
||||
self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level),
|
||||
alc_level=str(self.radio_alc))
|
||||
elif 0.2 < self.radio_alc < 0.99:
|
||||
print("0.2 < self.radio_alc < 0.99")
|
||||
self.tx_audio_level = self.tx_audio_level - 20
|
||||
self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level),
|
||||
alc_level=str(self.radio_alc))
|
||||
elif 1.0 >= self.radio_alc:
|
||||
print("1.0 >= self.radio_alc")
|
||||
self.tx_audio_level = self.tx_audio_level - 40
|
||||
self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level),
|
||||
alc_level=str(self.radio_alc))
|
||||
else:
|
||||
self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level),
|
||||
alc_level=str(self.radio_alc))
|
||||
|
||||
def transmit(
|
||||
self, mode, repeats: int, repeat_delay: int, frames: bytearray
|
||||
) -> bool:
|
||||
"""
|
||||
|
||||
Args:
|
||||
mode:
|
||||
repeats:
|
||||
repeat_delay:
|
||||
frames:
|
||||
|
||||
"""
|
||||
if TESTMODE:
|
||||
return
|
||||
|
||||
|
||||
self.demodulator.reset_data_sync()
|
||||
# get freedv instance by mode
|
||||
mode_transition = {
|
||||
codec2.FREEDV_MODE.signalling: self.freedv_datac13_tx,
|
||||
codec2.FREEDV_MODE.datac0: self.freedv_datac0_tx,
|
||||
codec2.FREEDV_MODE.datac1: self.freedv_datac1_tx,
|
||||
codec2.FREEDV_MODE.datac3: self.freedv_datac3_tx,
|
||||
codec2.FREEDV_MODE.datac4: self.freedv_datac4_tx,
|
||||
codec2.FREEDV_MODE.datac13: self.freedv_datac13_tx,
|
||||
}
|
||||
if mode in mode_transition:
|
||||
freedv = mode_transition[mode]
|
||||
else:
|
||||
print("wrong mode.................")
|
||||
print(mode)
|
||||
return False
|
||||
|
||||
# Wait for some other thread that might be transmitting
|
||||
self.states.waitForTransmission()
|
||||
self.states.setTransmitting(True)
|
||||
#self.states.channel_busy_event.wait()
|
||||
|
||||
|
||||
start_of_transmission = time.time()
|
||||
|
||||
# Open codec2 instance
|
||||
self.MODE = mode
|
||||
|
||||
txbuffer = bytes()
|
||||
|
||||
# Add empty data to handle ptt toggle time
|
||||
if self.tx_delay > 0:
|
||||
self.transmit_add_silence(txbuffer, self.tx_delay)
|
||||
|
||||
self.log.debug(
|
||||
"[MDM] TRANSMIT", mode=self.MODE.name, delay=self.tx_delay
|
||||
)
|
||||
|
||||
if not isinstance(frames, list): frames = [frames]
|
||||
for _ in range(repeats):
|
||||
|
||||
# Create modulation for all frames in the list
|
||||
for frame in frames:
|
||||
|
||||
txbuffer = self.transmit_add_preamble(txbuffer, freedv)
|
||||
txbuffer = self.transmit_create_frame(txbuffer, freedv, frame)
|
||||
txbuffer = self.transmit_add_postamble(txbuffer, freedv)
|
||||
|
||||
# Add delay to end of frames
|
||||
self.transmit_add_silence(txbuffer, repeat_delay)
|
||||
|
||||
# Re-sample back up to 48k (resampler works on np.int16)
|
||||
x = np.frombuffer(txbuffer, dtype=np.int16)
|
||||
|
||||
self.audio_auto_tune()
|
||||
x = audio.set_audio_volume(x, self.tx_audio_level)
|
||||
|
||||
if self.radiocontrol not in ["tci"]:
|
||||
txbuffer_out = self.resampler.resample8_to_48(x)
|
||||
else:
|
||||
txbuffer_out = x
|
||||
|
||||
# transmit audio
|
||||
self.transmit_audio(txbuffer_out)
|
||||
|
||||
self.radio.set_ptt(False)
|
||||
self.event_manager.send_ptt_change(False)
|
||||
self.states.setTransmitting(False)
|
||||
|
||||
end_of_transmission = time.time()
|
||||
transmission_time = end_of_transmission - start_of_transmission
|
||||
self.log.debug("[MDM] ON AIR TIME", time=transmission_time)
|
||||
return True
|
||||
|
||||
def transmit_add_preamble(self, buffer, freedv):
|
||||
|
||||
# Init buffer for preample
|
||||
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(
|
||||
freedv
|
||||
)
|
||||
mod_out_preamble = ctypes.create_string_buffer(n_tx_preamble_modem_samples * 2)
|
||||
|
||||
# Write preamble to txbuffer
|
||||
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
|
||||
buffer += bytes(mod_out_preamble)
|
||||
return buffer
|
||||
|
||||
def transmit_add_postamble(self, buffer, freedv):
|
||||
# Init buffer for postamble
|
||||
n_tx_postamble_modem_samples = (
|
||||
codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
|
||||
)
|
||||
mod_out_postamble = ctypes.create_string_buffer(
|
||||
n_tx_postamble_modem_samples * 2
|
||||
)
|
||||
# Write postamble to txbuffer
|
||||
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
|
||||
# Append postamble to txbuffer
|
||||
buffer += bytes(mod_out_postamble)
|
||||
return buffer
|
||||
|
||||
def transmit_add_silence(self, buffer, duration):
|
||||
data_delay = int(self.MODEM_SAMPLE_RATE * (duration / 1000)) # type: ignore
|
||||
mod_out_silence = ctypes.create_string_buffer(data_delay * 2)
|
||||
buffer += bytes(mod_out_silence)
|
||||
return buffer
|
||||
|
||||
def transmit_create_frame(self, txbuffer, freedv, frame):
|
||||
# Get number of bytes per frame for mode
|
||||
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8)
|
||||
payload_bytes_per_frame = bytes_per_frame - 2
|
||||
|
||||
# Init buffer for data
|
||||
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
|
||||
mod_out = ctypes.create_string_buffer(n_tx_modem_samples * 2)
|
||||
|
||||
# Create buffer for data
|
||||
# Use this if CRC16 checksum is required (DATAc1-3)
|
||||
buffer = bytearray(payload_bytes_per_frame)
|
||||
# Set buffersize to length of data which will be send
|
||||
buffer[: len(frame)] = frame # type: ignore
|
||||
|
||||
# Create crc for data frame -
|
||||
# Use the crc function shipped with codec2
|
||||
# to avoid CRC algorithm incompatibilities
|
||||
# Generate CRC16
|
||||
crc = ctypes.c_ushort(
|
||||
codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)
|
||||
)
|
||||
# Convert crc to 2-byte (16-bit) hex string
|
||||
crc = crc.value.to_bytes(2, byteorder="big")
|
||||
# Append CRC to data buffer
|
||||
buffer += crc
|
||||
|
||||
assert (bytes_per_frame == len(buffer))
|
||||
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
||||
# modulate DATA and save it into mod_out pointer
|
||||
codec2.api.freedv_rawdatatx(freedv, mod_out, data)
|
||||
txbuffer += bytes(mod_out)
|
||||
return txbuffer
|
||||
|
||||
def transmit_morse(self, repeats, repeat_delay, frames):
|
||||
self.states.waitForTransmission()
|
||||
|
@ -383,36 +201,54 @@ class RF:
|
|||
)
|
||||
start_of_transmission = time.time()
|
||||
|
||||
txbuffer_out = cw.MorseCodePlayer().text_to_signal("DJ2LS-1")
|
||||
txbuffer_out = cw.MorseCodePlayer().text_to_signal(self.config['STATION'].mycall)
|
||||
|
||||
self.transmit_audio(txbuffer_out)
|
||||
self.radio.set_ptt(False)
|
||||
self.event_manager.send_ptt_change(False)
|
||||
|
||||
self.states.setTransmitting(False)
|
||||
# transmit audio
|
||||
self.enqueue_audio_out(txbuffer_out)
|
||||
|
||||
end_of_transmission = time.time()
|
||||
transmission_time = end_of_transmission - start_of_transmission
|
||||
self.log.debug("[MDM] ON AIR TIME", time=transmission_time)
|
||||
|
||||
def init_codec2(self):
|
||||
# Open codec2 instances
|
||||
|
||||
# INIT TX MODES - here we need all modes.
|
||||
self.freedv_datac0_tx = codec2.open_instance(codec2.FREEDV_MODE.datac0.value)
|
||||
self.freedv_datac1_tx = codec2.open_instance(codec2.FREEDV_MODE.datac1.value)
|
||||
self.freedv_datac3_tx = codec2.open_instance(codec2.FREEDV_MODE.datac3.value)
|
||||
self.freedv_datac4_tx = codec2.open_instance(codec2.FREEDV_MODE.datac4.value)
|
||||
self.freedv_datac13_tx = codec2.open_instance(codec2.FREEDV_MODE.datac13.value)
|
||||
def transmit(
|
||||
self, mode, repeats: int, repeat_delay: int, frames: bytearray
|
||||
) -> bool:
|
||||
|
||||
def init_data_threads(self):
|
||||
worker_received = threading.Thread(
|
||||
target=self.demodulator.worker_received, name="WORKER_THREAD", daemon=True
|
||||
)
|
||||
worker_received.start()
|
||||
self.demodulator.reset_data_sync()
|
||||
|
||||
# Wait for some other thread that might be transmitting
|
||||
self.states.waitForTransmission()
|
||||
self.states.setTransmitting(True)
|
||||
# self.states.channel_busy_event.wait()
|
||||
|
||||
start_of_transmission = time.time()
|
||||
txbuffer = self.modulator.create_burst(mode, repeats, repeat_delay, frames)
|
||||
|
||||
# Re-sample back up to 48k (resampler works on np.int16)
|
||||
x = np.frombuffer(txbuffer, dtype=np.int16)
|
||||
x = audio.set_audio_volume(x, self.tx_audio_level)
|
||||
|
||||
if self.radiocontrol not in ["tci"]:
|
||||
txbuffer_out = self.resampler.resample8_to_48(x)
|
||||
else:
|
||||
txbuffer_out = x
|
||||
|
||||
# transmit audio
|
||||
self.enqueue_audio_out(txbuffer_out)
|
||||
|
||||
end_of_transmission = time.time()
|
||||
transmission_time = end_of_transmission - start_of_transmission
|
||||
self.log.debug("[MDM] ON AIR TIME", time=transmission_time)
|
||||
|
||||
|
||||
|
||||
def enqueue_audio_out(self, audio_48k) -> None:
|
||||
self.enqueuing_audio = True
|
||||
|
||||
if not self.states.isTransmitting():
|
||||
self.states.setTransmitting(True)
|
||||
|
||||
# Low level modem audio transmit
|
||||
def transmit_audio(self, audio_48k) -> None:
|
||||
self.radio.set_ptt(True)
|
||||
self.event_manager.send_ptt_change(True)
|
||||
|
||||
|
@ -421,5 +257,72 @@ class RF:
|
|||
# we need to wait manually for tci processing
|
||||
self.tci_module.wait_until_transmitted(audio_48k)
|
||||
else:
|
||||
sd.play(audio_48k, blocksize=4096, blocking=True)
|
||||
# slice audio data to needed blocklength
|
||||
block_size = 4800
|
||||
pad_length = -len(audio_48k) % block_size
|
||||
padded_data = np.pad(audio_48k, (0, pad_length), mode='constant')
|
||||
sliced_audio_data = padded_data.reshape(-1, block_size)
|
||||
# add each block to audio out queue
|
||||
for block in sliced_audio_data:
|
||||
self.audio_out_queue.put(block)
|
||||
|
||||
self.enqueuing_audio = False
|
||||
self.states.transmitting_event.wait()
|
||||
|
||||
self.radio.set_ptt(False)
|
||||
self.event_manager.send_ptt_change(False)
|
||||
|
||||
return
|
||||
|
||||
def sd_output_audio_callback(self, outdata: np.ndarray, frames: int, time, status) -> None:
|
||||
try:
|
||||
if not self.audio_out_queue.empty():
|
||||
chunk = self.audio_out_queue.get_nowait()
|
||||
audio.calculate_fft(chunk, self.fft_queue, self.states)
|
||||
outdata[:] = chunk.reshape(outdata.shape)
|
||||
|
||||
else:
|
||||
# reset transmitting state only, if we are not actively processing audio
|
||||
# for avoiding a ptt toggle state bug
|
||||
if not self.enqueuing_audio:
|
||||
self.states.setTransmitting(False)
|
||||
# Fill with zeros if the queue is empty
|
||||
outdata.fill(0)
|
||||
except Exception as e:
|
||||
self.log.warning("[AUDIO STATUS]", status=status, time=time, frames=frames, e=e)
|
||||
outdata.fill(0)
|
||||
|
||||
def sd_input_audio_callback(self, indata: np.ndarray, frames: int, time, status) -> None:
|
||||
if status:
|
||||
self.log.warning("[AUDIO STATUS]", status=status, time=time, frames=frames)
|
||||
# FIXME on windows input overflows crashing the rx audio stream. Lets restart the server then
|
||||
#if status.input_overflow:
|
||||
# self.service_queue.put("restart")
|
||||
return
|
||||
try:
|
||||
audio_48k = np.frombuffer(indata, dtype=np.int16)
|
||||
audio_8k = self.resampler.resample48_to_8(audio_48k)
|
||||
|
||||
audio_8k_level_adjusted = audio.set_audio_volume(audio_8k, self.rx_audio_level)
|
||||
|
||||
if not self.states.isTransmitting():
|
||||
audio.calculate_fft(audio_8k_level_adjusted, self.fft_queue, self.states)
|
||||
|
||||
length_audio_8k_level_adjusted = len(audio_8k_level_adjusted)
|
||||
# Avoid buffer overflow by filling only if buffer for
|
||||
# selected datachannel mode is not full
|
||||
index = 0
|
||||
for mode in self.demodulator.MODE_DICT:
|
||||
mode_data = self.demodulator.MODE_DICT[mode]
|
||||
audiobuffer = mode_data['audio_buffer']
|
||||
decode = mode_data['decode']
|
||||
index += 1
|
||||
if audiobuffer:
|
||||
if (audiobuffer.nbuffer + length_audio_8k_level_adjusted) > audiobuffer.size:
|
||||
self.demodulator.buffer_overflow_counter[index] += 1
|
||||
self.event_manager.send_buffer_overflow(self.demodulator.buffer_overflow_counter)
|
||||
elif decode:
|
||||
audiobuffer.push(audio_8k_level_adjusted)
|
||||
except Exception as e:
|
||||
self.log.warning("[AUDIO EXCEPTION]", status=status, time=time, frames=frames, e=e)
|
||||
|
||||
|
|
|
@ -6,9 +6,6 @@ from enum import Enum
|
|||
|
||||
class FRAME_TYPE(Enum):
|
||||
"""Lookup for frame types"""
|
||||
ARQ_CONNECTION_OPEN = 1
|
||||
ARQ_CONNECTION_HB = 2
|
||||
ARQ_CONNECTION_CLOSE = 3
|
||||
ARQ_STOP = 10
|
||||
ARQ_STOP_ACK = 11
|
||||
ARQ_SESSION_OPEN = 12
|
||||
|
@ -17,6 +14,14 @@ class FRAME_TYPE(Enum):
|
|||
ARQ_SESSION_INFO_ACK = 15
|
||||
ARQ_BURST_FRAME = 20
|
||||
ARQ_BURST_ACK = 21
|
||||
P2P_CONNECTION_CONNECT = 30
|
||||
P2P_CONNECTION_CONNECT_ACK = 31
|
||||
P2P_CONNECTION_HEARTBEAT = 32
|
||||
P2P_CONNECTION_HEARTBEAT_ACK = 33
|
||||
P2P_CONNECTION_PAYLOAD = 34
|
||||
P2P_CONNECTION_PAYLOAD_ACK = 35
|
||||
P2P_CONNECTION_DISCONNECT = 36
|
||||
P2P_CONNECTION_DISCONNECT_ACK = 37
|
||||
MESH_BROADCAST = 100
|
||||
MESH_SIGNALLING_PING = 101
|
||||
MESH_SIGNALLING_PING_ACK = 102
|
||||
|
|
150
modem/modulator.py
Normal file
150
modem/modulator.py
Normal file
|
@ -0,0 +1,150 @@
|
|||
import ctypes
|
||||
import codec2
|
||||
import structlog
|
||||
|
||||
|
||||
class Modulator:
|
||||
log = structlog.get_logger("RF")
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.tx_delay = config['MODEM']['tx_delay']
|
||||
self.modem_sample_rate = codec2.api.FREEDV_FS_8000
|
||||
|
||||
# Initialize codec2, rig control, and data threads
|
||||
self.init_codec2()
|
||||
|
||||
def init_codec2(self):
|
||||
# Open codec2 instances
|
||||
|
||||
# INIT TX MODES - here we need all modes.
|
||||
self.freedv_datac0_tx = codec2.open_instance(codec2.FREEDV_MODE.datac0.value)
|
||||
self.freedv_datac1_tx = codec2.open_instance(codec2.FREEDV_MODE.datac1.value)
|
||||
self.freedv_datac3_tx = codec2.open_instance(codec2.FREEDV_MODE.datac3.value)
|
||||
self.freedv_datac4_tx = codec2.open_instance(codec2.FREEDV_MODE.datac4.value)
|
||||
self.freedv_datac13_tx = codec2.open_instance(codec2.FREEDV_MODE.datac13.value)
|
||||
|
||||
def transmit_add_preamble(self, buffer, freedv):
|
||||
# Init buffer for preample
|
||||
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(
|
||||
freedv
|
||||
)
|
||||
mod_out_preamble = ctypes.create_string_buffer(n_tx_preamble_modem_samples * 2)
|
||||
|
||||
# Write preamble to txbuffer
|
||||
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
|
||||
buffer += bytes(mod_out_preamble)
|
||||
return buffer
|
||||
|
||||
def transmit_add_postamble(self, buffer, freedv):
|
||||
# Init buffer for postamble
|
||||
n_tx_postamble_modem_samples = (
|
||||
codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
|
||||
)
|
||||
mod_out_postamble = ctypes.create_string_buffer(
|
||||
n_tx_postamble_modem_samples * 2
|
||||
)
|
||||
# Write postamble to txbuffer
|
||||
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
|
||||
# Append postamble to txbuffer
|
||||
buffer += bytes(mod_out_postamble)
|
||||
return buffer
|
||||
|
||||
def transmit_add_silence(self, buffer, duration):
|
||||
data_delay = int(self.modem_sample_rate * (duration / 1000)) # type: ignore
|
||||
mod_out_silence = ctypes.create_string_buffer(data_delay * 2)
|
||||
buffer += bytes(mod_out_silence)
|
||||
return buffer
|
||||
|
||||
def transmit_create_frame(self, txbuffer, freedv, frame):
|
||||
# Get number of bytes per frame for mode
|
||||
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8)
|
||||
payload_bytes_per_frame = bytes_per_frame - 2
|
||||
|
||||
# Init buffer for data
|
||||
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
|
||||
mod_out = ctypes.create_string_buffer(n_tx_modem_samples * 2)
|
||||
|
||||
# Create buffer for data
|
||||
# Use this if CRC16 checksum is required (DATAc1-3)
|
||||
buffer = bytearray(payload_bytes_per_frame)
|
||||
# Set buffersize to length of data which will be send
|
||||
buffer[: len(frame)] = frame # type: ignore
|
||||
|
||||
# Create crc for data frame -
|
||||
# Use the crc function shipped with codec2
|
||||
# to avoid CRC algorithm incompatibilities
|
||||
# Generate CRC16
|
||||
crc = ctypes.c_ushort(
|
||||
codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)
|
||||
)
|
||||
# Convert crc to 2-byte (16-bit) hex string
|
||||
crc = crc.value.to_bytes(2, byteorder="big")
|
||||
# Append CRC to data buffer
|
||||
buffer += crc
|
||||
|
||||
assert (bytes_per_frame == len(buffer))
|
||||
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
||||
# modulate DATA and save it into mod_out pointer
|
||||
codec2.api.freedv_rawdatatx(freedv, mod_out, data)
|
||||
txbuffer += bytes(mod_out)
|
||||
return txbuffer
|
||||
|
||||
def create_burst(
|
||||
self, mode, repeats: int, repeat_delay: int, frames: bytearray
|
||||
) -> bool:
|
||||
"""
|
||||
|
||||
Args:
|
||||
mode:
|
||||
repeats:
|
||||
repeat_delay:
|
||||
frames:
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
# get freedv instance by mode
|
||||
mode_transition = {
|
||||
codec2.FREEDV_MODE.signalling: self.freedv_datac13_tx,
|
||||
codec2.FREEDV_MODE.datac0: self.freedv_datac0_tx,
|
||||
codec2.FREEDV_MODE.datac1: self.freedv_datac1_tx,
|
||||
codec2.FREEDV_MODE.datac3: self.freedv_datac3_tx,
|
||||
codec2.FREEDV_MODE.datac4: self.freedv_datac4_tx,
|
||||
codec2.FREEDV_MODE.datac13: self.freedv_datac13_tx,
|
||||
}
|
||||
if mode in mode_transition:
|
||||
freedv = mode_transition[mode]
|
||||
else:
|
||||
print("wrong mode.................")
|
||||
print(mode)
|
||||
return False
|
||||
|
||||
|
||||
# Open codec2 instance
|
||||
self.MODE = mode
|
||||
self.log.debug(
|
||||
"[MDM] TRANSMIT", mode=self.MODE.name, delay=self.tx_delay
|
||||
)
|
||||
|
||||
txbuffer = bytes()
|
||||
|
||||
# Add empty data to handle ptt toggle time
|
||||
if self.tx_delay > 0:
|
||||
txbuffer = self.transmit_add_silence(txbuffer, self.tx_delay)
|
||||
|
||||
if not isinstance(frames, list): frames = [frames]
|
||||
for _ in range(repeats):
|
||||
|
||||
# Create modulation for all frames in the list
|
||||
for frame in frames:
|
||||
txbuffer = self.transmit_add_preamble(txbuffer, freedv)
|
||||
txbuffer = self.transmit_create_frame(txbuffer, freedv, frame)
|
||||
txbuffer = self.transmit_add_postamble(txbuffer, freedv)
|
||||
|
||||
# Add delay to end of frames
|
||||
txbuffer = self.transmit_add_silence(txbuffer, repeat_delay)
|
||||
|
||||
return txbuffer
|
||||
|
315
modem/p2p_connection.py
Normal file
315
modem/p2p_connection.py
Normal file
|
@ -0,0 +1,315 @@
|
|||
import threading
|
||||
from enum import Enum
|
||||
from modem_frametypes import FRAME_TYPE
|
||||
from codec2 import FREEDV_MODE
|
||||
import data_frame_factory
|
||||
import structlog
|
||||
import random
|
||||
from queue import Queue
|
||||
import time
|
||||
from command_arq_raw import ARQRawCommand
|
||||
import numpy as np
|
||||
import base64
|
||||
from arq_data_type_handler import ARQDataTypeHandler, ARQ_SESSION_TYPES
|
||||
from arq_session_iss import ARQSessionISS
|
||||
|
||||
class States(Enum):
|
||||
NEW = 0
|
||||
CONNECTING = 1
|
||||
CONNECT_SENT = 2
|
||||
CONNECT_ACK_SENT = 3
|
||||
CONNECTED = 4
|
||||
#HEARTBEAT_SENT = 5
|
||||
#HEARTBEAT_ACK_SENT = 6
|
||||
PAYLOAD_SENT = 7
|
||||
ARQ_SESSION = 8
|
||||
DISCONNECTING = 9
|
||||
DISCONNECTED = 10
|
||||
FAILED = 11
|
||||
|
||||
|
||||
|
||||
class P2PConnection:
|
||||
STATE_TRANSITION = {
|
||||
States.NEW: {
|
||||
FRAME_TYPE.P2P_CONNECTION_CONNECT.value: 'connected_irs',
|
||||
},
|
||||
States.CONNECTING: {
|
||||
FRAME_TYPE.P2P_CONNECTION_CONNECT_ACK.value: 'connected_iss',
|
||||
},
|
||||
States.CONNECTED: {
|
||||
FRAME_TYPE.P2P_CONNECTION_CONNECT.value: 'connected_irs',
|
||||
FRAME_TYPE.P2P_CONNECTION_CONNECT_ACK.value: 'connected_iss',
|
||||
FRAME_TYPE.P2P_CONNECTION_PAYLOAD.value: 'received_data',
|
||||
FRAME_TYPE.P2P_CONNECTION_DISCONNECT.value: 'received_disconnect',
|
||||
},
|
||||
States.PAYLOAD_SENT: {
|
||||
FRAME_TYPE.P2P_CONNECTION_PAYLOAD_ACK.value: 'transmitted_data',
|
||||
},
|
||||
States.DISCONNECTING: {
|
||||
FRAME_TYPE.P2P_CONNECTION_DISCONNECT_ACK.value: 'received_disconnect_ack',
|
||||
},
|
||||
States.DISCONNECTED: {
|
||||
FRAME_TYPE.P2P_CONNECTION_DISCONNECT.value: 'received_disconnect',
|
||||
FRAME_TYPE.P2P_CONNECTION_DISCONNECT_ACK.value: 'received_disconnect_ack',
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, config: dict, modem, origin: str, destination: str, state_manager, event_manager, socket_command_handler=None):
|
||||
self.logger = structlog.get_logger(type(self).__name__)
|
||||
self.config = config
|
||||
|
||||
self.frame_factory = data_frame_factory.DataFrameFactory(self.config)
|
||||
|
||||
self.socket_command_handler = socket_command_handler
|
||||
|
||||
self.destination = destination
|
||||
self.origin = origin
|
||||
self.bandwidth = 0
|
||||
|
||||
self.state_manager = state_manager
|
||||
self.event_manager = event_manager
|
||||
self.modem = modem
|
||||
self.modem.demodulator.set_decode_mode()
|
||||
|
||||
self.p2p_data_rx_queue = Queue()
|
||||
self.p2p_data_tx_queue = Queue()
|
||||
|
||||
self.arq_data_type_handler = ARQDataTypeHandler(self.event_manager, self.state_manager)
|
||||
|
||||
|
||||
self.state = States.NEW
|
||||
self.session_id = self.generate_id()
|
||||
|
||||
self.event_frame_received = threading.Event()
|
||||
|
||||
self.RETRIES_CONNECT = 5
|
||||
self.TIMEOUT_CONNECT = 5
|
||||
self.TIMEOUT_DATA = 5
|
||||
self.RETRIES_DATA = 5
|
||||
self.ENTIRE_CONNECTION_TIMEOUT = 100
|
||||
|
||||
self.is_ISS = False # Indicator, if we are ISS or IRS
|
||||
|
||||
self.last_data_timestamp= time.time()
|
||||
self.start_data_processing_worker()
|
||||
|
||||
|
||||
def start_data_processing_worker(self):
|
||||
"""Starts a worker thread to monitor the transmit data queue and process data."""
|
||||
|
||||
def data_processing_worker():
|
||||
while True:
|
||||
if time.time() > self.last_data_timestamp + self.ENTIRE_CONNECTION_TIMEOUT and self.state is not States.ARQ_SESSION:
|
||||
self.disconnect()
|
||||
return
|
||||
|
||||
if not self.p2p_data_tx_queue.empty() and self.state == States.CONNECTED:
|
||||
self.process_data_queue()
|
||||
threading.Event().wait(0.1)
|
||||
|
||||
|
||||
|
||||
|
||||
# Create and start the worker thread
|
||||
worker_thread = threading.Thread(target=data_processing_worker, daemon=True)
|
||||
worker_thread.start()
|
||||
|
||||
def generate_id(self):
|
||||
while True:
|
||||
random_int = random.randint(1,255)
|
||||
if random_int not in self.state_manager.p2p_connection_sessions:
|
||||
return random_int
|
||||
|
||||
if len(self.state_manager.p2p_connection_sessions) >= 255:
|
||||
return False
|
||||
|
||||
def set_details(self, snr, frequency_offset):
|
||||
self.snr = snr
|
||||
self.frequency_offset = frequency_offset
|
||||
|
||||
def log(self, message, isWarning = False):
|
||||
msg = f"[{type(self).__name__}][id={self.session_id}][state={self.state}][ISS={bool(self.is_ISS)}]: {message}"
|
||||
logger = self.logger.warn if isWarning else self.logger.info
|
||||
logger(msg)
|
||||
|
||||
def set_state(self, state):
|
||||
if self.state == state:
|
||||
self.log(f"{type(self).__name__} state {self.state.name} unchanged.")
|
||||
else:
|
||||
self.log(f"{type(self).__name__} state change from {self.state.name} to {state.name}")
|
||||
self.state = state
|
||||
|
||||
def on_frame_received(self, frame):
|
||||
self.last_data_timestamp = time.time()
|
||||
self.event_frame_received.set()
|
||||
self.log(f"Received {frame['frame_type']}")
|
||||
frame_type = frame['frame_type_int']
|
||||
if self.state in self.STATE_TRANSITION:
|
||||
if frame_type in self.STATE_TRANSITION[self.state]:
|
||||
action_name = self.STATE_TRANSITION[self.state][frame_type]
|
||||
response = getattr(self, action_name)(frame)
|
||||
|
||||
return
|
||||
|
||||
self.log(f"Ignoring unknown transition from state {self.state.name} with frame {frame['frame_type']}")
|
||||
|
||||
def transmit_frame(self, frame: bytearray, mode='auto'):
|
||||
self.log("Transmitting frame")
|
||||
if mode in ['auto']:
|
||||
mode = self.get_mode_by_speed_level(self.speed_level)
|
||||
|
||||
self.modem.transmit(mode, 1, 1, frame)
|
||||
|
||||
def transmit_wait_and_retry(self, frame_or_burst, timeout, retries, mode):
|
||||
while retries > 0:
|
||||
self.event_frame_received = threading.Event()
|
||||
if isinstance(frame_or_burst, list): burst = frame_or_burst
|
||||
else: burst = [frame_or_burst]
|
||||
for f in burst:
|
||||
self.transmit_frame(f, mode)
|
||||
self.event_frame_received.clear()
|
||||
self.log(f"Waiting {timeout} seconds...")
|
||||
if self.event_frame_received.wait(timeout):
|
||||
return
|
||||
self.log("Timeout!")
|
||||
retries = retries - 1
|
||||
|
||||
#self.connected_iss() # override connection state for simulation purposes
|
||||
self.session_failed()
|
||||
|
||||
def launch_twr(self, frame_or_burst, timeout, retries, mode):
|
||||
twr = threading.Thread(target = self.transmit_wait_and_retry, args=[frame_or_burst, timeout, retries, mode], daemon=True)
|
||||
twr.start()
|
||||
|
||||
def transmit_and_wait_irs(self, frame, timeout, mode):
|
||||
self.event_frame_received.clear()
|
||||
self.transmit_frame(frame, mode)
|
||||
self.log(f"Waiting {timeout} seconds...")
|
||||
#if not self.event_frame_received.wait(timeout):
|
||||
# self.log("Timeout waiting for ISS. Session failed.")
|
||||
# self.transmission_failed()
|
||||
|
||||
def launch_twr_irs(self, frame, timeout, mode):
|
||||
thread_wait = threading.Thread(target = self.transmit_and_wait_irs,
|
||||
args = [frame, timeout, mode], daemon=True)
|
||||
thread_wait.start()
|
||||
|
||||
def connect(self):
|
||||
self.set_state(States.CONNECTING)
|
||||
self.is_ISS = True
|
||||
session_open_frame = self.frame_factory.build_p2p_connection_connect(self.origin, self.destination, self.session_id)
|
||||
self.launch_twr(session_open_frame, self.TIMEOUT_CONNECT, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)
|
||||
return
|
||||
|
||||
def connected_iss(self, frame=None):
|
||||
self.log("CONNECTED ISS...........................")
|
||||
self.set_state(States.CONNECTED)
|
||||
self.is_ISS = True
|
||||
if self.socket_command_handler:
|
||||
self.socket_command_handler.socket_respond_connected(self.origin, self.destination, self.bandwidth)
|
||||
|
||||
def connected_irs(self, frame):
|
||||
self.log("CONNECTED IRS...........................")
|
||||
self.state_manager.register_p2p_connection_session(self)
|
||||
self.set_state(States.CONNECTED)
|
||||
self.is_ISS = False
|
||||
self.orign = frame["origin"]
|
||||
self.destination = frame["destination_crc"]
|
||||
|
||||
if self.socket_command_handler:
|
||||
self.socket_command_handler.socket_respond_connected(self.origin, self.destination, self.bandwidth)
|
||||
|
||||
session_open_frame = self.frame_factory.build_p2p_connection_connect_ack(self.destination, self.origin, self.session_id)
|
||||
self.launch_twr_irs(session_open_frame, self.ENTIRE_CONNECTION_TIMEOUT, mode=FREEDV_MODE.signalling)
|
||||
|
||||
def session_failed(self):
|
||||
self.set_state(States.FAILED)
|
||||
if self.socket_command_handler:
|
||||
self.socket_command_handler.socket_respond_disconnected()
|
||||
|
||||
def process_data_queue(self, frame=None):
|
||||
if not self.p2p_data_tx_queue.empty():
|
||||
print("processing data....")
|
||||
|
||||
self.set_state(States.PAYLOAD_SENT)
|
||||
data = self.p2p_data_tx_queue.get()
|
||||
sequence_id = random.randint(0,255)
|
||||
data = data.encode('utf-8')
|
||||
|
||||
if len(data) <= 11:
|
||||
mode = FREEDV_MODE.signalling
|
||||
elif 11 < len(data) < 32:
|
||||
mode = FREEDV_MODE.datac4
|
||||
else:
|
||||
self.transmit_arq(data)
|
||||
return
|
||||
|
||||
payload = self.frame_factory.build_p2p_connection_payload(mode, self.session_id, sequence_id, data)
|
||||
self.launch_twr(payload, self.TIMEOUT_DATA, self.RETRIES_DATA,mode=mode)
|
||||
return
|
||||
|
||||
def prepare_data_chunk(self, data, mode):
|
||||
return data
|
||||
|
||||
def received_data(self, frame):
|
||||
print(frame)
|
||||
self.p2p_data_rx_queue.put(frame['data'])
|
||||
|
||||
ack_data = self.frame_factory.build_p2p_connection_payload_ack(self.session_id, 0)
|
||||
self.launch_twr_irs(ack_data, self.ENTIRE_CONNECTION_TIMEOUT, mode=FREEDV_MODE.signalling)
|
||||
|
||||
def transmit_data_ack(self, frame):
|
||||
print(frame)
|
||||
|
||||
def transmitted_data(self, frame):
|
||||
print("transmitted data...")
|
||||
self.set_state(States.CONNECTED)
|
||||
|
||||
def disconnect(self):
|
||||
if self.state not in [States.DISCONNECTING, States.DISCONNECTED]:
|
||||
self.set_state(States.DISCONNECTING)
|
||||
disconnect_frame = self.frame_factory.build_p2p_connection_disconnect(self.session_id)
|
||||
self.launch_twr(disconnect_frame, self.TIMEOUT_CONNECT, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)
|
||||
return
|
||||
|
||||
def received_disconnect(self, frame):
|
||||
self.log("DISCONNECTED...............")
|
||||
self.set_state(States.DISCONNECTED)
|
||||
if self.socket_command_handler:
|
||||
self.socket_command_handler.socket_respond_disconnected()
|
||||
self.is_ISS = False
|
||||
disconnect_ack_frame = self.frame_factory.build_p2p_connection_disconnect_ack(self.session_id)
|
||||
self.launch_twr_irs(disconnect_ack_frame, self.ENTIRE_CONNECTION_TIMEOUT, mode=FREEDV_MODE.signalling)
|
||||
|
||||
def received_disconnect_ack(self, frame):
|
||||
self.log("DISCONNECTED...............")
|
||||
self.set_state(States.DISCONNECTED)
|
||||
if self.socket_command_handler:
|
||||
self.socket_command_handler.socket_respond_disconnected()
|
||||
|
||||
def transmit_arq(self, data):
|
||||
self.set_state(States.ARQ_SESSION)
|
||||
|
||||
print("----------------------------------------------------------------")
|
||||
print(self.destination)
|
||||
print(self.state_manager.p2p_connection_sessions)
|
||||
|
||||
prepared_data, type_byte = self.arq_data_type_handler.prepare(data, ARQ_SESSION_TYPES.p2p_connection)
|
||||
iss = ARQSessionISS(self.config, self.modem, 'AA1AAA-1', self.state_manager, prepared_data, type_byte)
|
||||
iss.id = self.session_id
|
||||
if iss.id:
|
||||
self.state_manager.register_arq_iss_session(iss)
|
||||
iss.start()
|
||||
return iss
|
||||
|
||||
def transmitted_arq(self):
|
||||
self.last_data_timestamp = time.time()
|
||||
self.set_state(States.CONNECTED)
|
||||
|
||||
def received_arq(self, data):
|
||||
self.last_data_timestamp = time.time()
|
||||
self.set_state(States.CONNECTED)
|
||||
self.p2p_data_rx_queue.put(data)
|
||||
|
105
modem/rigctld.py
105
modem/rigctld.py
|
@ -76,7 +76,11 @@ class radio:
|
|||
self.connection.sendall(command.encode('utf-8') + b"\n")
|
||||
response = self.connection.recv(1024)
|
||||
self.await_response.set()
|
||||
return response.decode('utf-8').strip()
|
||||
stripped_result = response.decode('utf-8').strip()
|
||||
if 'RPRT' in stripped_result:
|
||||
return None
|
||||
return stripped_result
|
||||
|
||||
except Exception as err:
|
||||
self.log.warning(f"[RIGCTLD] Error sending command [{command}] to rigctld: {err}")
|
||||
self.connected = False
|
||||
|
@ -189,36 +193,93 @@ class radio:
|
|||
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:
|
||||
self.get_frequency()
|
||||
self.get_mode_bandwidth()
|
||||
self.get_alc()
|
||||
self.get_strength()
|
||||
self.get_rf()
|
||||
|
||||
return self.parameters
|
||||
|
||||
def get_frequency(self):
|
||||
try:
|
||||
frequency_response = self.send_command('f')
|
||||
self.parameters['frequency'] = frequency_response if frequency_response is not None else 'err'
|
||||
except Exception as e:
|
||||
self.log.warning(f"Error getting frequency: {e}")
|
||||
self.parameters['frequency'] = 'err'
|
||||
|
||||
def get_mode_bandwidth(self):
|
||||
try:
|
||||
response = self.send_command('m')
|
||||
if response is not None:
|
||||
response = response.strip()
|
||||
mode, bandwidth = response.split('\n', 1)
|
||||
else:
|
||||
mode = 'err'
|
||||
bandwidth = 'err'
|
||||
|
||||
except ValueError:
|
||||
mode = 'err'
|
||||
bandwidth = 'err'
|
||||
except Exception as e:
|
||||
self.log.warning(f"Error getting mode and bandwidth: {e}")
|
||||
mode = 'err'
|
||||
bandwidth = 'err'
|
||||
finally:
|
||||
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'] = int(float(self.send_command('l RFPOWER')) * 100) # RF, RFPOWER
|
||||
def get_alc(self):
|
||||
try:
|
||||
alc_response = self.send_command('l ALC')
|
||||
self.parameters['alc'] = alc_response if alc_response is not None else 'err'
|
||||
except Exception as e:
|
||||
self.log.warning(f"Error getting ALC: {e}")
|
||||
self.parameters['alc'] = 'err'
|
||||
|
||||
"""Return the latest fetched parameters."""
|
||||
return self.parameters
|
||||
def get_strength(self):
|
||||
try:
|
||||
strength_response = self.send_command('l STRENGTH')
|
||||
self.parameters['strength'] = strength_response if strength_response is not None else 'err'
|
||||
except Exception as e:
|
||||
self.log.warning(f"Error getting strength: {e}")
|
||||
self.parameters['strength'] = 'err'
|
||||
|
||||
def get_rf(self):
|
||||
try:
|
||||
rf_response = self.send_command('l RFPOWER')
|
||||
if rf_response is not None:
|
||||
self.parameters['rf'] = int(float(rf_response) * 100)
|
||||
else:
|
||||
self.parameters['rf'] = 'err'
|
||||
except ValueError:
|
||||
self.parameters['rf'] = 'err'
|
||||
except Exception as e:
|
||||
self.log.warning(f"Error getting RF power: {e}")
|
||||
self.parameters['rf'] = 'err'
|
||||
|
||||
def start_service(self):
|
||||
binary_name = "rigctld"
|
||||
binary_path = helpers.find_binary_path(binary_name, search_system_wide=True)
|
||||
binary_paths = helpers.find_binary_paths(binary_name, search_system_wide=True)
|
||||
additional_args = self.format_rigctld_args()
|
||||
if binary_path:
|
||||
self.log.info(f"Rigctld binary found at: {binary_path}")
|
||||
helpers.kill_and_execute(binary_path, additional_args)
|
||||
self.log.info(f"Executed rigctld...")
|
||||
|
||||
if binary_paths:
|
||||
for binary_path in binary_paths:
|
||||
try:
|
||||
self.log.info(f"Attempting to start rigctld using binary found at: {binary_path}")
|
||||
helpers.kill_and_execute(binary_path, additional_args)
|
||||
self.log.info("Successfully executed rigctld.")
|
||||
break # Exit the loop after successful execution
|
||||
except Exception as e:
|
||||
pass
|
||||
# let's keep this hidden for the user to avoid confusion
|
||||
# self.log.warning(f"Failed to start rigctld with binary at {binary_path}: {e}")
|
||||
else:
|
||||
self.log.warning("Failed to start rigctld with all found binaries.", binaries=binary_paths)
|
||||
else:
|
||||
self.log.warning("Rigctld binary not found.")
|
||||
|
||||
|
||||
def format_rigctld_args(self):
|
||||
config = self.config['RADIO'] # Accessing the 'RADIO' section of the INI file
|
||||
config_rigctld = self.config['RIGCTLD'] # Accessing the 'RIGCTLD' section of the INI file for custom args
|
||||
|
@ -238,13 +299,13 @@ class radio:
|
|||
|
||||
# PTT Port and Type
|
||||
if not should_ignore(config.get('ptt_port')):
|
||||
args += ['--ptt-port', config['ptt_port']]
|
||||
args += ['-p', config['ptt_port']]
|
||||
if not should_ignore(config.get('ptt_type')):
|
||||
args += ['--ptt-type', config['ptt_type']]
|
||||
args += ['-P', config['ptt_type']]
|
||||
|
||||
# Serial DCD and DTR
|
||||
if not should_ignore(config.get('serial_dcd')):
|
||||
args += ['--dcd-type', config['serial_dcd']]
|
||||
args += ['-D', config['serial_dcd']]
|
||||
|
||||
if not should_ignore(config.get('serial_dtr')):
|
||||
args += ['--set-conf', f'dtr_state={config["serial_dtr"]}']
|
||||
|
@ -262,7 +323,7 @@ class radio:
|
|||
# Handle custom arguments for rigctld
|
||||
# Custom args are split via ' ' so python doesn't add extranaeous quotes on windows
|
||||
args += config_rigctld["arguments"].split(" ")
|
||||
#print("Hamlib args ==>" + str(args))
|
||||
print("Hamlib args ==>" + str(args))
|
||||
|
||||
return args
|
||||
|
||||
|
|
|
@ -16,13 +16,12 @@ class ScheduleManager:
|
|||
self.state_manager = state_manger
|
||||
self.event_manager = event_manager
|
||||
self.config = self.config_manager.read()
|
||||
self.beacon_interval = self.config['MODEM']['beacon_interval']
|
||||
|
||||
self.scheduler = sched.scheduler(time.time, time.sleep)
|
||||
self.events = {
|
||||
'check_for_queued_messages': {'function': self.check_for_queued_messages, 'interval': 10},
|
||||
'explorer_publishing': {'function': self.push_to_explorer, 'interval': 60},
|
||||
'transmitting_beacon': {'function': self.transmit_beacon, 'interval': self.beacon_interval},
|
||||
'transmitting_beacon': {'function': self.transmit_beacon, 'interval': 600},
|
||||
'beacon_cleanup': {'function': self.delete_beacons, 'interval': 600},
|
||||
}
|
||||
self.running = False # Flag to control the running state
|
||||
|
@ -65,24 +64,36 @@ class ScheduleManager:
|
|||
self.scheduler_thread.join()
|
||||
|
||||
def transmit_beacon(self):
|
||||
if not self.state_manager.getARQ() and self.state_manager.is_beacon_running:
|
||||
cmd = command_beacon.BeaconCommand(self.config, self.state_manager, self.event_manager)
|
||||
cmd.run(self.event_manager, self.modem)
|
||||
try:
|
||||
if not self.state_manager.getARQ() and self.state_manager.is_beacon_running:
|
||||
cmd = command_beacon.BeaconCommand(self.config, self.state_manager, self.event_manager)
|
||||
cmd.run(self.event_manager, self.modem)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def delete_beacons(self):
|
||||
DatabaseManagerBeacon(self.event_manager).beacon_cleanup_older_than_days(2)
|
||||
try:
|
||||
DatabaseManagerBeacon(self.event_manager).beacon_cleanup_older_than_days(2)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def push_to_explorer(self):
|
||||
self.config = self.config_manager.read()
|
||||
if self.config['STATION']['enable_explorer']:
|
||||
explorer.explorer(self.modem_version, self.config_manager, self.state_manager).push()
|
||||
try:
|
||||
explorer.explorer(self.modem_version, self.config_manager, self.state_manager).push()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def check_for_queued_messages(self):
|
||||
if not self.state_manager.getARQ():
|
||||
if DatabaseManagerMessages(self.event_manager).get_first_queued_message():
|
||||
params = DatabaseManagerMessages(self.event_manager).get_first_queued_message()
|
||||
command = command_message_send.SendMessageCommand(self.config_manager.read(), self.state_manager, self.event_manager, params)
|
||||
command.transmit(self.modem)
|
||||
|
||||
try:
|
||||
if first_queued_message := DatabaseManagerMessages(
|
||||
self.event_manager
|
||||
).get_first_queued_message():
|
||||
command = command_message_send.SendMessageCommand(self.config_manager.read(), self.state_manager, self.event_manager, first_queued_message)
|
||||
command.transmit(self.modem)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import time
|
||||
|
||||
from flask import Flask, request, jsonify, make_response, abort, Response
|
||||
from flask_sock import Sock
|
||||
from flask_cors import CORS
|
||||
|
@ -9,16 +11,19 @@ import audio
|
|||
import queue
|
||||
import service_manager
|
||||
import state_manager
|
||||
import ujson as json
|
||||
import json
|
||||
import websocket_manager as wsm
|
||||
import api_validations as validations
|
||||
import command_cq
|
||||
import command_beacon
|
||||
import command_ping
|
||||
import command_feq
|
||||
import command_test
|
||||
import command_arq_raw
|
||||
import command_message_send
|
||||
import event_manager
|
||||
import atexit
|
||||
|
||||
from message_system_db_manager import DatabaseManager
|
||||
from message_system_db_messages import DatabaseManagerMessages
|
||||
from message_system_db_attachments import DatabaseManagerAttachments
|
||||
|
@ -26,17 +31,17 @@ from message_system_db_beacon import DatabaseManagerBeacon
|
|||
from schedule_manager import ScheduleManager
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
CORS(app, resources={r"/*": {"origins": "*"}})
|
||||
sock = Sock(app)
|
||||
MODEM_VERSION = "0.13.6-alpha"
|
||||
MODEM_VERSION = "0.14.5-alpha"
|
||||
|
||||
# set config file to use
|
||||
def set_config():
|
||||
if 'FREEDATA_CONFIG' in os.environ:
|
||||
config_file = os.environ['FREEDATA_CONFIG']
|
||||
else:
|
||||
config_file = 'config.ini'
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
config_file = os.path.join(script_dir, 'config.ini')
|
||||
|
||||
if os.path.exists(config_file):
|
||||
print(f"Using config from {config_file}")
|
||||
|
@ -96,6 +101,13 @@ def index():
|
|||
@app.route('/config', methods=['GET', 'POST'])
|
||||
def config():
|
||||
if request.method in ['POST']:
|
||||
|
||||
if not validations.validate_remote_config(request.json):
|
||||
return api_abort("wrong config", 500)
|
||||
# check if config already exists
|
||||
if app.config_manager.read() == request.json:
|
||||
return api_response(request.json)
|
||||
|
||||
set_config = app.config_manager.write(request.json)
|
||||
if not set_config:
|
||||
response = api_response(None, 'error writing config')
|
||||
|
@ -142,6 +154,8 @@ def post_beacon():
|
|||
|
||||
if not app.state_manager.is_beacon_running:
|
||||
app.state_manager.set('is_beacon_running', request.json['enabled'])
|
||||
if not app.state_manager.getARQ():
|
||||
enqueue_tx_command(command_beacon.BeaconCommand, request.json)
|
||||
else:
|
||||
app.state_manager.set('is_beacon_running', request.json['enabled'])
|
||||
|
||||
|
@ -310,7 +324,20 @@ def sock_fft(sock):
|
|||
def sock_states(sock):
|
||||
wsm.handle_connection(sock, wsm.states_client_list, app.state_queue)
|
||||
|
||||
@atexit.register
|
||||
def stop_server():
|
||||
try:
|
||||
app.service_manager.modem_service.put("stop")
|
||||
app.socket_interface_manager.stop_servers()
|
||||
|
||||
if app.service_manager.modem:
|
||||
app.service_manager.modem.sd_input_stream.stop
|
||||
audio.sd._terminate()
|
||||
except Exception as e:
|
||||
print("Error stopping modem")
|
||||
time.sleep(1)
|
||||
print("------------------------------------------")
|
||||
print('Server shutdown...')
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.config['SOCK_SERVER_OPTIONS'] = {'ping_interval': 10}
|
||||
|
@ -321,6 +348,7 @@ if __name__ == "__main__":
|
|||
app.config_manager = CONFIG(config_file)
|
||||
|
||||
# start modem
|
||||
app.p2p_data_queue = queue.Queue() # queue which holds processing data of p2p connections
|
||||
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
|
||||
|
@ -332,9 +360,21 @@ if __name__ == "__main__":
|
|||
app.schedule_manager = ScheduleManager(app.MODEM_VERSION, app.config_manager, app.state_manager, app.event_manager)
|
||||
# start service manager
|
||||
app.service_manager = service_manager.SM(app)
|
||||
|
||||
# start modem service
|
||||
app.modem_service.put("start")
|
||||
# initialize database default values
|
||||
DatabaseManager(app.event_manager).initialize_default_values()
|
||||
wsm.startThreads(app)
|
||||
app.run()
|
||||
|
||||
conf = app.config_manager.read()
|
||||
modemaddress = conf['NETWORK']['modemaddress']
|
||||
modemport = conf['NETWORK']['modemport']
|
||||
|
||||
if not modemaddress:
|
||||
modemaddress = '127.0.0.1'
|
||||
if not modemport:
|
||||
modemport = 5000
|
||||
|
||||
app.run(modemaddress, modemport)
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import structlog
|
|||
import audio
|
||||
|
||||
import radio_manager
|
||||
|
||||
from socket_interface import SocketInterfaceHandler
|
||||
|
||||
class SM:
|
||||
def __init__(self, app):
|
||||
|
@ -19,7 +19,7 @@ class SM:
|
|||
self.state_manager = app.state_manager
|
||||
self.event_manager = app.event_manager
|
||||
self.schedule_manager = app.schedule_manager
|
||||
|
||||
self.socket_interface_manager = None
|
||||
|
||||
runner_thread = threading.Thread(
|
||||
target=self.runner, name="runner thread", daemon=True
|
||||
|
@ -34,15 +34,23 @@ class SM:
|
|||
self.start_radio_manager()
|
||||
self.start_modem()
|
||||
|
||||
if self.config['SOCKET_INTERFACE']['enable']:
|
||||
self.socket_interface_manager = SocketInterfaceHandler(self.modem, self.app.config_manager, self.state_manager, self.event_manager).start_servers()
|
||||
|
||||
elif cmd in ['stop'] and self.modem:
|
||||
self.stop_modem()
|
||||
self.stop_radio_manager()
|
||||
if self.config['SOCKET_INTERFACE']['enable']:
|
||||
self.socket_interface_manager.stop_servers()
|
||||
# we need to wait a bit for avoiding a portaudio crash
|
||||
threading.Event().wait(0.5)
|
||||
|
||||
elif cmd in ['restart']:
|
||||
self.stop_modem()
|
||||
self.stop_radio_manager()
|
||||
if self.config['SOCKET_INTERFACE']['enable']:
|
||||
self.socket_interface_manager.stop_servers()
|
||||
|
||||
# we need to wait a bit for avoiding a portaudio crash
|
||||
threading.Event().wait(0.5)
|
||||
|
||||
|
|
193
modem/socket_interface.py
Normal file
193
modem/socket_interface.py
Normal file
|
@ -0,0 +1,193 @@
|
|||
""" WORK IN PROGRESS by DJ2LS"""
|
||||
|
||||
import socketserver
|
||||
import threading
|
||||
import structlog
|
||||
import select
|
||||
from queue import Queue
|
||||
from socket_interface_commands import SocketCommandHandler
|
||||
|
||||
|
||||
class CommandSocket(socketserver.BaseRequestHandler):
|
||||
#def __init__(self, request, client_address, server):
|
||||
def __init__(self, request, client_address, server, modem=None, state_manager=None, event_manager=None, config_manager=None):
|
||||
self.state_manager = state_manager
|
||||
self.event_manager = event_manager
|
||||
self.config_manager = config_manager
|
||||
self.modem = modem
|
||||
self.logger = structlog.get_logger(type(self).__name__)
|
||||
|
||||
self.command_handler = SocketCommandHandler(request, self.modem, self.config_manager, self.state_manager, self.event_manager)
|
||||
|
||||
self.handlers = {
|
||||
'CONNECT': self.command_handler.handle_connect,
|
||||
'DISCONNECT': self.command_handler.handle_disconnect,
|
||||
'MYCALL': self.command_handler.handle_mycall,
|
||||
'BW': self.command_handler.handle_bw,
|
||||
'ABORT': self.command_handler.handle_abort,
|
||||
'PUBLIC': self.command_handler.handle_public,
|
||||
'CWID': self.command_handler.handle_cwid,
|
||||
'LISTEN': self.command_handler.handle_listen,
|
||||
'COMPRESSION': self.command_handler.handle_compression,
|
||||
'WINLINK SESSION': self.command_handler.handle_winlink_session,
|
||||
}
|
||||
super().__init__(request, client_address, server)
|
||||
|
||||
def log(self, message, isWarning = False):
|
||||
msg = f"[{type(self).__name__}]: {message}"
|
||||
logger = self.logger.warn if isWarning else self.logger.info
|
||||
logger(msg)
|
||||
|
||||
def handle(self):
|
||||
self.log(f"Client connected: {self.client_address}")
|
||||
try:
|
||||
while True:
|
||||
data = self.request.recv(1024).strip()
|
||||
if not data:
|
||||
break
|
||||
decoded_data = data.decode()
|
||||
self.log(f"Command received from {self.client_address}: {decoded_data}")
|
||||
self.parse_command(decoded_data)
|
||||
finally:
|
||||
self.log(f"Command connection closed with {self.client_address}")
|
||||
|
||||
def parse_command(self, data):
|
||||
for command in self.handlers:
|
||||
if data.startswith(command):
|
||||
# Extract command arguments after the command itself
|
||||
args = data[len(command):].strip().split()
|
||||
self.dispatch_command(command, args)
|
||||
return
|
||||
self.send_response("ERROR: Unknown command\r\n")
|
||||
|
||||
def dispatch_command(self, command, data):
|
||||
if command in self.handlers:
|
||||
handler = self.handlers[command]
|
||||
handler(data)
|
||||
else:
|
||||
self.send_response(f"Unknown command: {command}")
|
||||
|
||||
|
||||
|
||||
class DataSocket(socketserver.BaseRequestHandler):
|
||||
#def __init__(self, request, client_address, server):
|
||||
def __init__(self, request, client_address, server, modem=None, state_manager=None, event_manager=None, config_manager=None):
|
||||
self.state_manager = state_manager
|
||||
self.event_manager = event_manager
|
||||
self.config_manager = config_manager
|
||||
self.modem = modem
|
||||
|
||||
self.logger = structlog.get_logger(type(self).__name__)
|
||||
|
||||
super().__init__(request, client_address, server)
|
||||
|
||||
def log(self, message, isWarning = False):
|
||||
msg = f"[{type(self).__name__}]: {message}"
|
||||
logger = self.logger.warn if isWarning else self.logger.info
|
||||
logger(msg)
|
||||
|
||||
def handle(self):
|
||||
self.log(f"Data connection established with {self.client_address}")
|
||||
|
||||
try:
|
||||
while True:
|
||||
|
||||
ready_to_read, _, _ = select.select([self.request], [], [], 1) # 1-second timeout
|
||||
if ready_to_read:
|
||||
self.data = self.request.recv(1024).strip()
|
||||
if not self.data:
|
||||
break
|
||||
try:
|
||||
self.log(f"Data received from {self.client_address}: [{len(self.data)}] - {self.data.decode()}")
|
||||
except Exception:
|
||||
self.log(f"Data received from {self.client_address}: [{len(self.data)}] - {self.data}")
|
||||
|
||||
for session in self.state_manager.p2p_connection_sessions:
|
||||
print(f"sessions: {session}")
|
||||
session.p2p_data_tx_queue.put(self.data)
|
||||
|
||||
# Check if there's something to send from the queue, without blocking
|
||||
|
||||
for session_id in self.state_manager.p2p_connection_sessions:
|
||||
session = self.state_manager.get_p2p_connection_session(session_id)
|
||||
if not session.p2p_data_tx_queue.empty():
|
||||
data_to_send = session.p2p_data_tx_queue.get_nowait() # Use get_nowait to avoid blocking
|
||||
self.request.sendall(data_to_send)
|
||||
self.log(f"Sent data to {self.client_address}")
|
||||
|
||||
finally:
|
||||
self.log(f"Data connection closed with {self.client_address}")
|
||||
|
||||
|
||||
#class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
# allow_reuse_address = True
|
||||
|
||||
|
||||
class CustomThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
allow_reuse_address = True
|
||||
|
||||
def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True, **kwargs):
|
||||
self.extra_args = kwargs
|
||||
super().__init__(server_address, RequestHandlerClass, bind_and_activate=bind_and_activate)
|
||||
|
||||
def finish_request(self, request, client_address):
|
||||
self.RequestHandlerClass(request, client_address, self, **self.extra_args)
|
||||
|
||||
class SocketInterfaceHandler:
|
||||
def __init__(self, modem, config_manager, state_manager, event_manager):
|
||||
self.modem = modem
|
||||
self.config_manager = config_manager
|
||||
self.config = self.config_manager.read()
|
||||
self.state_manager = state_manager
|
||||
self.event_manager = event_manager
|
||||
self.logger = structlog.get_logger(type(self).__name__)
|
||||
self.command_port = self.config["SOCKET_INTERFACE"]["cmd_port"]
|
||||
self.data_port = self.config["SOCKET_INTERFACE"]["data_port"]
|
||||
self.command_server = None
|
||||
self.data_server = None
|
||||
self.command_server_thread = None
|
||||
self.data_server_thread = None
|
||||
|
||||
def log(self, message, isWarning = False):
|
||||
msg = f"[{type(self).__name__}]: {message}"
|
||||
logger = self.logger.warn if isWarning else self.logger.info
|
||||
logger(msg)
|
||||
|
||||
def start_servers(self):
|
||||
|
||||
if self.command_port == 0:
|
||||
self.command_port = 8300
|
||||
|
||||
if self.data_port == 0:
|
||||
self.data_port = 8301
|
||||
|
||||
# Method to start both command and data server threads
|
||||
self.command_server_thread = threading.Thread(target=self.run_server, args=(self.command_port, CommandSocket))
|
||||
self.data_server_thread = threading.Thread(target=self.run_server, args=(self.data_port, DataSocket))
|
||||
|
||||
self.command_server_thread.start()
|
||||
self.data_server_thread.start()
|
||||
|
||||
self.log(f"Interfaces started")
|
||||
|
||||
def run_server(self, port, handler):
|
||||
with CustomThreadedTCPServer(('127.0.0.1', port), handler, modem=self.modem, state_manager=self.state_manager, event_manager=self.event_manager, config_manager=self.config_manager) as server:
|
||||
self.log(f"Server started on port {port}")
|
||||
if port == self.command_port:
|
||||
self.command_server = server
|
||||
else:
|
||||
self.data_server = server
|
||||
server.serve_forever()
|
||||
|
||||
def stop_servers(self):
|
||||
# Gracefully shutdown the server
|
||||
if self.command_server:
|
||||
self.command_server.shutdown()
|
||||
if self.data_server:
|
||||
self.data_server.shutdown()
|
||||
self.log(f"Interfaces stopped")
|
||||
|
||||
def wait_for_server_threads(self):
|
||||
# Wait for both server threads to finish
|
||||
self.command_server_thread.join()
|
||||
self.data_server_thread.join()
|
76
modem/socket_interface_commands.py
Normal file
76
modem/socket_interface_commands.py
Normal file
|
@ -0,0 +1,76 @@
|
|||
""" WORK IN PROGRESS by DJ2LS"""
|
||||
from command_p2p_connection import P2PConnectionCommand
|
||||
|
||||
class SocketCommandHandler:
|
||||
|
||||
def __init__(self, cmd_request, modem, config_manager, state_manager, event_manager):
|
||||
self.cmd_request = cmd_request
|
||||
self.modem = modem
|
||||
self.config_manager = config_manager
|
||||
self.state_manager = state_manager
|
||||
self.event_manager = event_manager
|
||||
|
||||
self.session = None
|
||||
|
||||
def send_response(self, message):
|
||||
full_message = f"{message}\r\n"
|
||||
self.cmd_request.sendall(full_message.encode())
|
||||
|
||||
def handle_connect(self, data):
|
||||
|
||||
params = {
|
||||
'origin': data[0],
|
||||
'destination': data[1],
|
||||
}
|
||||
cmd = P2PConnectionCommand(self.config_manager.read(), self.state_manager, self.event_manager, params, self)
|
||||
self.session = cmd.run(self.event_manager.queues, self.modem)
|
||||
|
||||
#if self.session.session_id:
|
||||
# self.state_manager.register_p2p_connection_session(self.session)
|
||||
# self.send_response("OK")
|
||||
# self.session.connect()
|
||||
#else:
|
||||
# self.send_response("ERROR")
|
||||
|
||||
def handle_disconnect(self, data):
|
||||
# Your existing connect logic
|
||||
self.send_response("OK")
|
||||
|
||||
def handle_mycall(self, data):
|
||||
# Logic for handling MYCALL command
|
||||
self.send_response("OK")
|
||||
|
||||
def handle_bw(self, data):
|
||||
# Logic for handling BW command
|
||||
self.send_response("OK")
|
||||
|
||||
def handle_abort(self, data):
|
||||
# Logic for handling ABORT command
|
||||
self.send_response("OK")
|
||||
|
||||
def handle_public(self, data):
|
||||
# Logic for handling PUBLIC command
|
||||
self.send_response("OK")
|
||||
|
||||
def handle_cwid(self, data):
|
||||
# Logic for handling CWID command
|
||||
self.send_response("OK")
|
||||
|
||||
def handle_listen(self, data):
|
||||
# Logic for handling LISTEN command
|
||||
self.send_response("OK")
|
||||
|
||||
def handle_compression(self, data):
|
||||
# Logic for handling COMPRESSION command
|
||||
self.send_response("OK")
|
||||
|
||||
def handle_winlink_session(self, data):
|
||||
# Logic for handling WINLINK SESSION command
|
||||
self.send_response("OK")
|
||||
|
||||
def socket_respond_disconnected(self):
|
||||
self.send_response("DISCONNECTED")
|
||||
|
||||
def socket_respond_connected(self, mycall, dxcall, bandwidth):
|
||||
message = f"CONNECTED {mycall} {dxcall} {bandwidth}"
|
||||
self.send_response(message)
|
|
@ -1,5 +1,4 @@
|
|||
import time
|
||||
import ujson as json
|
||||
import threading
|
||||
import numpy as np
|
||||
class StateManager:
|
||||
|
@ -39,6 +38,8 @@ class StateManager:
|
|||
self.arq_iss_sessions = {}
|
||||
self.arq_irs_sessions = {}
|
||||
|
||||
self.p2p_connection_sessions = {}
|
||||
|
||||
#self.mesh_routing_table = []
|
||||
|
||||
self.radio_frequency = 0
|
||||
|
@ -215,3 +216,15 @@ class StateManager:
|
|||
"radio_rf_level": self.radio_rf_level,
|
||||
"s_meter_strength": self.s_meter_strength,
|
||||
}
|
||||
|
||||
def register_p2p_connection_session(self, session):
|
||||
if session.session_id in self.p2p_connection_sessions:
|
||||
print("session already registered...")
|
||||
return False
|
||||
self.p2p_connection_sessions[session.session_id] = session
|
||||
return True
|
||||
|
||||
def get_p2p_connection_session(self, id):
|
||||
if id not in self.p2p_connection_sessions:
|
||||
pass
|
||||
return self.p2p_connection_sessions[id]
|
|
@ -8,7 +8,7 @@ Created on 05.11.23
|
|||
# pylint: disable=import-outside-toplevel, attribute-defined-outside-init
|
||||
|
||||
import requests
|
||||
import ujson as json
|
||||
import json
|
||||
import structlog
|
||||
|
||||
log = structlog.get_logger("stats")
|
||||
|
|
|
@ -5,7 +5,6 @@ PyAudio
|
|||
pyserial
|
||||
sounddevice
|
||||
structlog
|
||||
ujson
|
||||
requests
|
||||
chardet
|
||||
colorama
|
||||
|
|
|
@ -144,7 +144,7 @@ class TestARQSession(unittest.TestCase):
|
|||
self.waitAndCloseChannels()
|
||||
del cmd
|
||||
|
||||
def DisabledtestARQSessionLargePayload(self):
|
||||
def testARQSessionLargePayload(self):
|
||||
# set Packet Error Rate (PER) / frame loss probability
|
||||
self.loss_probability = 0
|
||||
|
||||
|
@ -221,7 +221,9 @@ class TestARQSession(unittest.TestCase):
|
|||
session = arq_session_irs.ARQSessionIRS(self.config,
|
||||
self.irs_modem,
|
||||
'AA1AAA-1',
|
||||
random.randint(0, 255))
|
||||
random.randint(0, 255),
|
||||
self.irs_state_manager
|
||||
)
|
||||
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]
|
||||
|
|
|
@ -32,7 +32,7 @@ class TestDataFrameFactory(unittest.TestCase):
|
|||
def testARQConnect(self):
|
||||
dxcall = "DJ2LS-4"
|
||||
session_id = 123
|
||||
frame = self.factory.build_arq_session_open(dxcall, session_id)
|
||||
frame = self.factory.build_arq_session_open(dxcall, session_id, 1700)
|
||||
frame_data = self.factory.deconstruct(frame)
|
||||
|
||||
self.assertEqual(frame_data['origin'], self.factory.myfullcall)
|
||||
|
@ -49,7 +49,7 @@ class TestDataFrameFactory(unittest.TestCase):
|
|||
offset = 40
|
||||
payload = b'Hello World!'
|
||||
frame = self.factory.build_arq_burst_frame(FREEDV_MODE.datac3,
|
||||
session_id, offset, payload)
|
||||
session_id, offset, payload, 0)
|
||||
frame_data = self.factory.deconstruct(frame)
|
||||
self.assertEqual(frame_data['session_id'], session_id)
|
||||
self.assertEqual(frame_data['offset'], offset)
|
||||
|
@ -58,11 +58,11 @@ class TestDataFrameFactory(unittest.TestCase):
|
|||
|
||||
payload = payload * 1000
|
||||
self.assertRaises(OverflowError, self.factory.build_arq_burst_frame,
|
||||
FREEDV_MODE.datac3, session_id, offset, payload)
|
||||
FREEDV_MODE.datac3, session_id, offset, payload, 0)
|
||||
|
||||
def testAvailablePayload(self):
|
||||
avail = self.factory.get_available_data_payload_for_mode(FRAME_TYPE.ARQ_BURST_FRAME, FREEDV_MODE.datac3)
|
||||
self.assertEqual(avail, 120) # 128 bytes datac3 frame payload - BURST frame overhead
|
||||
self.assertEqual(avail, 119) # 128 bytes datac3 frame payload - BURST frame overhead
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
@ -22,21 +22,21 @@ class TestDispatcher(unittest.TestCase):
|
|||
# Example usage
|
||||
example_data = b"Hello FreeDATA!"
|
||||
formatted_data, type_byte = self.arq_data_type_handler.prepare(example_data, ARQ_SESSION_TYPES.raw)
|
||||
dispatched_data = self.arq_data_type_handler.dispatch(type_byte, formatted_data)
|
||||
dispatched_data = self.arq_data_type_handler.dispatch(type_byte, formatted_data, statistics={})
|
||||
self.assertEqual(example_data, dispatched_data)
|
||||
|
||||
def testDataTypeHandlerLZMA(self):
|
||||
# Example usage
|
||||
example_data = b"Hello FreeDATA!"
|
||||
formatted_data, type_byte = self.arq_data_type_handler.prepare(example_data, ARQ_SESSION_TYPES.raw_lzma)
|
||||
dispatched_data = self.arq_data_type_handler.dispatch(type_byte, formatted_data)
|
||||
dispatched_data = self.arq_data_type_handler.dispatch(type_byte, formatted_data, statistics={})
|
||||
self.assertEqual(example_data, dispatched_data)
|
||||
|
||||
def testDataTypeHandlerGZIP(self):
|
||||
# Example usage
|
||||
example_data = b"Hello FreeDATA!"
|
||||
formatted_data, type_byte = self.arq_data_type_handler.prepare(example_data, ARQ_SESSION_TYPES.raw_gzip)
|
||||
dispatched_data = self.arq_data_type_handler.dispatch(type_byte, formatted_data)
|
||||
dispatched_data = self.arq_data_type_handler.dispatch(type_byte, formatted_data, statistics={})
|
||||
self.assertEqual(example_data, dispatched_data)
|
||||
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ class TestDataFrameFactory(unittest.TestCase):
|
|||
payload = message.to_payload()
|
||||
received_message = MessageP2P.from_payload(payload)
|
||||
received_message_dict = MessageP2P.to_dict(received_message)
|
||||
self.database_manager.add_message(received_message_dict)
|
||||
self.database_manager.add_message(received_message_dict, statistics={})
|
||||
result = self.database_manager.get_message_by_id(message.id)
|
||||
|
||||
self.assertEqual(result["destination"], message.destination)
|
||||
|
@ -53,7 +53,7 @@ class TestDataFrameFactory(unittest.TestCase):
|
|||
payload = message.to_payload()
|
||||
received_message = MessageP2P.from_payload(payload)
|
||||
received_message_dict = MessageP2P.to_dict(received_message)
|
||||
self.database_manager.add_message(received_message_dict)
|
||||
self.database_manager.add_message(received_message_dict, statistics={})
|
||||
|
||||
result = self.database_manager.get_all_messages()
|
||||
message_id = result[0]["id"]
|
||||
|
@ -75,7 +75,7 @@ class TestDataFrameFactory(unittest.TestCase):
|
|||
received_message = MessageP2P.from_payload(payload)
|
||||
received_message_dict = MessageP2P.to_dict(received_message)
|
||||
print(received_message_dict)
|
||||
message_id = self.database_manager.add_message(received_message_dict, direction='receive')
|
||||
message_id = self.database_manager.add_message(received_message_dict, statistics={}, direction='receive')
|
||||
print(message_id)
|
||||
self.database_manager.update_message(message_id, {'body' : 'hello123'})
|
||||
|
||||
|
@ -103,7 +103,7 @@ class TestDataFrameFactory(unittest.TestCase):
|
|||
payload = message.to_payload()
|
||||
received_message = MessageP2P.from_payload(payload)
|
||||
received_message_dict = MessageP2P.to_dict(received_message)
|
||||
message_id = self.database_manager.add_message(received_message_dict)
|
||||
message_id = self.database_manager.add_message(received_message_dict, statistics={})
|
||||
result = self.database_manager_attachments.get_attachments_by_message_id(message_id)
|
||||
attachment_names = [attachment['name'] for attachment in result]
|
||||
self.assertIn('test1.gif', attachment_names)
|
||||
|
@ -116,7 +116,7 @@ class TestDataFrameFactory(unittest.TestCase):
|
|||
payload = message.to_payload()
|
||||
received_message = MessageP2P.from_payload(payload)
|
||||
received_message_dict = MessageP2P.to_dict(received_message)
|
||||
message_id = self.database_manager.add_message(received_message_dict)
|
||||
message_id = self.database_manager.add_message(received_message_dict,statistics={},)
|
||||
self.database_manager.increment_message_attempts(message_id)
|
||||
|
||||
|
||||
|
@ -129,7 +129,7 @@ class TestDataFrameFactory(unittest.TestCase):
|
|||
payload = message.to_payload()
|
||||
received_message = MessageP2P.from_payload(payload)
|
||||
received_message_dict = MessageP2P.to_dict(received_message)
|
||||
message_id = self.database_manager.add_message(received_message_dict, is_read=False)
|
||||
message_id = self.database_manager.add_message(received_message_dict, statistics={},is_read=False)
|
||||
self.database_manager.mark_message_as_read(message_id)
|
||||
|
||||
result = self.database_manager.get_message_by_id(message_id)
|
||||
|
|
|
@ -60,7 +60,7 @@ class TestDataFrameFactory(unittest.TestCase):
|
|||
payload = message.to_payload()
|
||||
received_message = MessageP2P.from_payload(payload)
|
||||
received_message_dict = MessageP2P.to_dict(received_message)
|
||||
self.database_manager.add_message(received_message_dict)
|
||||
self.database_manager.add_message(received_message_dict, statistics={})
|
||||
|
||||
self.assertEqual(message.origin, received_message.origin)
|
||||
self.assertEqual(message.destination, received_message.destination)
|
||||
|
|
197
tests/test_p2p_connection.py
Normal file
197
tests/test_p2p_connection.py
Normal file
|
@ -0,0 +1,197 @@
|
|||
import sys
|
||||
import time
|
||||
|
||||
sys.path.append('modem')
|
||||
|
||||
import unittest
|
||||
import unittest.mock
|
||||
from config import CONFIG
|
||||
import helpers
|
||||
import queue
|
||||
import threading
|
||||
import base64
|
||||
from command_p2p_connection import P2PConnectionCommand
|
||||
from state_manager import StateManager
|
||||
from frame_dispatcher import DISPATCHER
|
||||
import random
|
||||
import structlog
|
||||
import numpy as np
|
||||
from event_manager import EventManager
|
||||
from state_manager import StateManager
|
||||
from data_frame_factory import DataFrameFactory
|
||||
import codec2
|
||||
import p2p_connection
|
||||
|
||||
from socket_interface_commands import SocketCommandHandler
|
||||
|
||||
class TestModem:
|
||||
def __init__(self, event_q, state_q):
|
||||
self.data_queue_received = queue.Queue()
|
||||
self.demodulator = unittest.mock.Mock()
|
||||
self.event_manager = EventManager([event_q])
|
||||
self.logger = structlog.get_logger('Modem')
|
||||
self.states = StateManager(state_q)
|
||||
|
||||
def getFrameTransmissionTime(self, mode):
|
||||
samples = 0
|
||||
c2instance = codec2.open_instance(mode.value)
|
||||
samples += codec2.api.freedv_get_n_tx_preamble_modem_samples(c2instance)
|
||||
samples += codec2.api.freedv_get_n_tx_modem_samples(c2instance)
|
||||
samples += codec2.api.freedv_get_n_tx_postamble_modem_samples(c2instance)
|
||||
time = samples / 8000
|
||||
return time
|
||||
|
||||
def transmit(self, mode, repeats: int, repeat_delay: int, frames: bytearray) -> bool:
|
||||
# Simulate transmission time
|
||||
tx_time = self.getFrameTransmissionTime(mode) + 0.1 # PTT
|
||||
self.logger.info(f"TX {tx_time} seconds...")
|
||||
threading.Event().wait(tx_time)
|
||||
|
||||
transmission = {
|
||||
'mode': mode,
|
||||
'bytes': frames,
|
||||
}
|
||||
self.data_queue_received.put(transmission)
|
||||
|
||||
|
||||
class TestP2PConnectionSession(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
config_manager = CONFIG('modem/config.ini.example')
|
||||
cls.config = config_manager.read()
|
||||
cls.logger = structlog.get_logger("TESTS")
|
||||
cls.frame_factory = DataFrameFactory(cls.config)
|
||||
|
||||
# ISS
|
||||
cls.iss_config_manager = config_manager
|
||||
cls.iss_state_manager = StateManager(queue.Queue())
|
||||
cls.iss_event_manager = EventManager([queue.Queue()])
|
||||
cls.iss_event_queue = queue.Queue()
|
||||
cls.iss_state_queue = queue.Queue()
|
||||
cls.iss_p2p_data_queue = queue.Queue()
|
||||
|
||||
|
||||
cls.iss_modem = TestModem(cls.iss_event_queue, cls.iss_state_queue)
|
||||
cls.iss_frame_dispatcher = DISPATCHER(cls.config,
|
||||
cls.iss_event_manager,
|
||||
cls.iss_state_manager,
|
||||
cls.iss_modem)
|
||||
|
||||
#cls.iss_socket_interface_handler = SocketInterfaceHandler(cls.iss_modem, cls.iss_config_manager, cls.iss_state_manager, cls.iss_event_manager)
|
||||
#cls.iss_socket_command_handler = CommandSocket(TestSocket(), '127.0.0.1', 51234)
|
||||
|
||||
# IRS
|
||||
cls.irs_state_manager = StateManager(queue.Queue())
|
||||
cls.irs_event_manager = EventManager([queue.Queue()])
|
||||
cls.irs_event_queue = queue.Queue()
|
||||
cls.irs_state_queue = queue.Queue()
|
||||
cls.irs_p2p_data_queue = queue.Queue()
|
||||
cls.irs_modem = TestModem(cls.irs_event_queue, cls.irs_state_queue)
|
||||
cls.irs_frame_dispatcher = DISPATCHER(cls.config,
|
||||
cls.irs_event_manager,
|
||||
cls.irs_state_manager,
|
||||
cls.irs_modem)
|
||||
|
||||
# Frame loss probability in %
|
||||
cls.loss_probability = 30
|
||||
|
||||
cls.channels_running = True
|
||||
|
||||
cls.disconnect_received = False
|
||||
|
||||
def channelWorker(self, modem_transmit_queue: queue.Queue, frame_dispatcher: DISPATCHER):
|
||||
while self.channels_running:
|
||||
# Transfer data between both parties
|
||||
try:
|
||||
transmission = modem_transmit_queue.get(timeout=1)
|
||||
if random.randint(0, 100) < self.loss_probability:
|
||||
self.logger.info(f"[{threading.current_thread().name}] Frame lost...")
|
||||
continue
|
||||
|
||||
frame_bytes = transmission['bytes']
|
||||
frame_dispatcher.new_process_data(frame_bytes, None, len(frame_bytes), 0, 0)
|
||||
except queue.Empty:
|
||||
continue
|
||||
self.logger.info(f"[{threading.current_thread().name}] Channel closed.")
|
||||
|
||||
def waitForSession(self, q, outbound=False):
|
||||
while True and self.channels_running:
|
||||
ev = q.get()
|
||||
print(ev)
|
||||
if 'P2P_CONNECTION_DISCONNECT_ACK' in ev or self.disconnect_received:
|
||||
self.logger.info(f"[{threading.current_thread().name}] session ended.")
|
||||
break
|
||||
|
||||
def establishChannels(self):
|
||||
self.channels_running = True
|
||||
self.iss_to_irs_channel = threading.Thread(target=self.channelWorker,
|
||||
args=[self.iss_modem.data_queue_received,
|
||||
self.irs_frame_dispatcher],
|
||||
name="ISS to IRS channel")
|
||||
self.iss_to_irs_channel.start()
|
||||
|
||||
self.irs_to_iss_channel = threading.Thread(target=self.channelWorker,
|
||||
args=[self.irs_modem.data_queue_received,
|
||||
self.iss_frame_dispatcher],
|
||||
name="IRS to ISS channel")
|
||||
self.irs_to_iss_channel.start()
|
||||
|
||||
def waitAndCloseChannels(self):
|
||||
self.waitForSession(self.iss_event_queue, True)
|
||||
self.channels_running = False
|
||||
self.waitForSession(self.irs_event_queue, False)
|
||||
self.channels_running = False
|
||||
|
||||
def generate_random_string(self, min_length, max_length):
|
||||
import string
|
||||
length = random.randint(min_length, max_length)
|
||||
return ''.join(random.choices(string.ascii_letters, k=length))#
|
||||
|
||||
def DisabledtestARQSessionSmallPayload(self):
|
||||
# set Packet Error Rate (PER) / frame loss probability
|
||||
self.loss_probability = 0
|
||||
|
||||
self.establishChannels()
|
||||
|
||||
handler = SocketCommandHandler(TestSocket(self), self.iss_modem, self.iss_config_manager, self.iss_state_manager, self.iss_event_manager)
|
||||
handler.handle_connect(["AA1AAA-1", "BB2BBB-2"])
|
||||
|
||||
self.connected_event = threading.Event()
|
||||
self.connected_event.wait()
|
||||
|
||||
for session_id in self.iss_state_manager.p2p_connection_sessions:
|
||||
session = self.iss_state_manager.get_p2p_connection_session(session_id)
|
||||
session.ENTIRE_CONNECTION_TIMEOUT = 15
|
||||
# Generate and add 5 random entries to the queue
|
||||
for _ in range(3):
|
||||
min_length = (30 * _ ) + 1
|
||||
max_length = (30 * _ ) + 1
|
||||
print(min_length)
|
||||
print(max_length)
|
||||
random_entry = self.generate_random_string(min_length, max_length)
|
||||
session.p2p_data_tx_queue.put(random_entry)
|
||||
|
||||
session.p2p_data_tx_queue.put('12345')
|
||||
self.waitAndCloseChannels()
|
||||
|
||||
|
||||
class TestSocket:
|
||||
def __init__(self, test_class):
|
||||
self.sent_data = [] # To capture data sent through this socket
|
||||
self.test_class = test_class
|
||||
def sendall(self, data):
|
||||
print(f"Mock sendall called with data: {data}")
|
||||
self.sent_data.append(data)
|
||||
self.event_handler(data)
|
||||
|
||||
def event_handler(self, data):
|
||||
if b'CONNECTED AA1AAA-1 BB2BBB-2 0\r\n' in self.sent_data:
|
||||
self.test_class.connected_event.set()
|
||||
|
||||
if b'DISCONNECTED\r\n' in self.sent_data:
|
||||
self.disconnect_received = True
|
||||
self.test_class.assertEqual(b'DISCONNECTED\r\n', b'DISCONNECTED\r\n')
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -48,7 +48,7 @@ class TestIntegration(unittest.TestCase):
|
|||
self.assertIn('RADIO', config)
|
||||
|
||||
def test_config_post(self):
|
||||
config = {'NETWORK': {'modemport' : 3050}}
|
||||
config = {'STATION': {'mygrid' : 'JN48ea'}}
|
||||
r = requests.post(self.url + '/config',
|
||||
headers={'Content-type': 'application/json'},
|
||||
data = json.dumps(config))
|
||||
|
@ -57,7 +57,7 @@ class TestIntegration(unittest.TestCase):
|
|||
r = requests.get(self.url + '/config')
|
||||
self.assertEqual(r.status_code, 200)
|
||||
config = r.json()
|
||||
self.assertEqual(config['NETWORK']['modemport'], 3050)
|
||||
self.assertEqual(config['NETWORK']['modemport'], 5000)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
Loading…
Reference in a new issue