diff --git a/.gitmodules b/.gitmodules
index 6d49c83..0c17aa7 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,6 +1,3 @@
-[submodule "WhackerLinkLib"]
- path = WhackerLinkLib
- url = https://github.com/whackerlink/WhackerLinkLib
[submodule "fnecore"]
path = fnecore
url = https://github.com/dvmproject/fnecore
diff --git a/WhackerLinkConsoleV2.sln b/DVMConsole.sln
similarity index 68%
rename from WhackerLinkConsoleV2.sln
rename to DVMConsole.sln
index ad8965d..38bd846 100644
--- a/WhackerLinkConsoleV2.sln
+++ b/DVMConsole.sln
@@ -1,112 +1,86 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.8.34330.188
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WhackerLinkLib", "WhackerLinkLib\WhackerLinkLib.csproj", "{5918329A-6374-40E2-874D-445360C89676}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WhackerLinkConsoleV2", "WhackerLinkConsoleV2\WhackerLinkConsoleV2.csproj", "{710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B5A7CF60-CCDE-4B2B-85C1-86AE3A19FB31}"
- ProjectSection(SolutionItems) = preProject
- .editorconfig = .editorconfig
- EndProjectSection
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "fnecore", "fnecore\fnecore.csproj", "{1F06ECB1-9928-1430-63F4-2E01522A0510}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Debug|x64 = Debug|x64
- Debug|x86 = Debug|x86
- NoVocode|Any CPU = NoVocode|Any CPU
- NoVocode|x64 = NoVocode|x64
- NoVocode|x86 = NoVocode|x86
- Release|Any CPU = Release|Any CPU
- Release|x64 = Release|x64
- Release|x86 = Release|x86
- WIN32|Any CPU = WIN32|Any CPU
- WIN32|x64 = WIN32|x64
- WIN32|x86 = WIN32|x86
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {5918329A-6374-40E2-874D-445360C89676}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5918329A-6374-40E2-874D-445360C89676}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5918329A-6374-40E2-874D-445360C89676}.Debug|x64.ActiveCfg = AmbeVocode|x64
- {5918329A-6374-40E2-874D-445360C89676}.Debug|x64.Build.0 = AmbeVocode|x64
- {5918329A-6374-40E2-874D-445360C89676}.Debug|x86.ActiveCfg = Debug|x86
- {5918329A-6374-40E2-874D-445360C89676}.Debug|x86.Build.0 = Debug|x86
- {5918329A-6374-40E2-874D-445360C89676}.NoVocode|Any CPU.ActiveCfg = NoVocode|Any CPU
- {5918329A-6374-40E2-874D-445360C89676}.NoVocode|Any CPU.Build.0 = NoVocode|Any CPU
- {5918329A-6374-40E2-874D-445360C89676}.NoVocode|x64.ActiveCfg = NoVocode|x64
- {5918329A-6374-40E2-874D-445360C89676}.NoVocode|x64.Build.0 = NoVocode|x64
- {5918329A-6374-40E2-874D-445360C89676}.NoVocode|x86.ActiveCfg = NoVocode|x86
- {5918329A-6374-40E2-874D-445360C89676}.NoVocode|x86.Build.0 = NoVocode|x86
- {5918329A-6374-40E2-874D-445360C89676}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5918329A-6374-40E2-874D-445360C89676}.Release|Any CPU.Build.0 = Release|Any CPU
- {5918329A-6374-40E2-874D-445360C89676}.Release|x64.ActiveCfg = Release|x64
- {5918329A-6374-40E2-874D-445360C89676}.Release|x64.Build.0 = Release|x64
- {5918329A-6374-40E2-874D-445360C89676}.Release|x86.ActiveCfg = Release|x86
- {5918329A-6374-40E2-874D-445360C89676}.Release|x86.Build.0 = Release|x86
- {5918329A-6374-40E2-874D-445360C89676}.WIN32|Any CPU.ActiveCfg = WIN32|Any CPU
- {5918329A-6374-40E2-874D-445360C89676}.WIN32|Any CPU.Build.0 = WIN32|Any CPU
- {5918329A-6374-40E2-874D-445360C89676}.WIN32|x64.ActiveCfg = WIN32|x64
- {5918329A-6374-40E2-874D-445360C89676}.WIN32|x64.Build.0 = WIN32|x64
- {5918329A-6374-40E2-874D-445360C89676}.WIN32|x86.ActiveCfg = WIN32|x86
- {5918329A-6374-40E2-874D-445360C89676}.WIN32|x86.Build.0 = WIN32|x86
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|x64.ActiveCfg = Debug|x64
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|x64.Build.0 = Debug|x64
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|x86.ActiveCfg = Debug|x86
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|x86.Build.0 = Debug|x86
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.NoVocode|Any CPU.ActiveCfg = Debug|Any CPU
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.NoVocode|Any CPU.Build.0 = Debug|Any CPU
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.NoVocode|x64.ActiveCfg = Debug|x64
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.NoVocode|x64.Build.0 = Debug|x64
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.NoVocode|x86.ActiveCfg = Debug|Any CPU
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.NoVocode|x86.Build.0 = Debug|Any CPU
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|Any CPU.Build.0 = Release|Any CPU
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|x64.ActiveCfg = Release|x64
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|x64.Build.0 = Release|x64
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|x86.ActiveCfg = Release|x86
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|x86.Build.0 = Release|x86
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|Any CPU.ActiveCfg = WIN32|Any CPU
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|Any CPU.Build.0 = WIN32|Any CPU
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|x64.ActiveCfg = WIN32|x64
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|x64.Build.0 = WIN32|x64
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|x86.ActiveCfg = WIN32|x86
- {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|x86.Build.0 = WIN32|x86
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.Debug|x64.ActiveCfg = Debug|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.Debug|x64.Build.0 = Debug|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.Debug|x86.ActiveCfg = Debug|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.Debug|x86.Build.0 = Debug|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.NoVocode|Any CPU.ActiveCfg = Release|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.NoVocode|Any CPU.Build.0 = Release|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.NoVocode|x64.ActiveCfg = Release|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.NoVocode|x64.Build.0 = Release|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.NoVocode|x86.ActiveCfg = Release|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.NoVocode|x86.Build.0 = Release|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|Any CPU.Build.0 = Release|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|x64.ActiveCfg = Release|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|x64.Build.0 = Release|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|x86.ActiveCfg = Release|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|x86.Build.0 = Release|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|Any CPU.ActiveCfg = WIN32|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|Any CPU.Build.0 = WIN32|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x64.ActiveCfg = Release|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x64.Build.0 = Release|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x86.ActiveCfg = Release|Any CPU
- {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x86.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {2891343C-A29A-4952-B9E1-2468A3DA70DC}
- EndGlobalSection
-EndGlobal
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.8.34330.188
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DVMConsole", "DVMConsole\DVMConsole.csproj", "{710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B5A7CF60-CCDE-4B2B-85C1-86AE3A19FB31}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "fnecore", "fnecore\fnecore.csproj", "{1F06ECB1-9928-1430-63F4-2E01522A0510}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ NoVocode|Any CPU = NoVocode|Any CPU
+ NoVocode|x64 = NoVocode|x64
+ NoVocode|x86 = NoVocode|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ WIN32|Any CPU = WIN32|Any CPU
+ WIN32|x64 = WIN32|x64
+ WIN32|x86 = WIN32|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|x64.ActiveCfg = Debug|x64
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|x64.Build.0 = Debug|x64
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|x86.ActiveCfg = Debug|x86
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|x86.Build.0 = Debug|x86
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.NoVocode|Any CPU.ActiveCfg = Debug|Any CPU
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.NoVocode|Any CPU.Build.0 = Debug|Any CPU
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.NoVocode|x64.ActiveCfg = Debug|x64
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.NoVocode|x64.Build.0 = Debug|x64
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.NoVocode|x86.ActiveCfg = Debug|Any CPU
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.NoVocode|x86.Build.0 = Debug|Any CPU
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|x64.ActiveCfg = Release|x64
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|x64.Build.0 = Release|x64
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|x86.ActiveCfg = Release|x86
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|x86.Build.0 = Release|x86
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|Any CPU.ActiveCfg = WIN32|Any CPU
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|Any CPU.Build.0 = WIN32|Any CPU
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|x64.ActiveCfg = WIN32|x64
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|x64.Build.0 = WIN32|x64
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|x86.ActiveCfg = WIN32|x86
+ {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|x86.Build.0 = WIN32|x86
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.Debug|x64.Build.0 = Debug|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.Debug|x86.Build.0 = Debug|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.NoVocode|Any CPU.ActiveCfg = Release|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.NoVocode|Any CPU.Build.0 = Release|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.NoVocode|x64.ActiveCfg = Release|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.NoVocode|x64.Build.0 = Release|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.NoVocode|x86.ActiveCfg = Release|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.NoVocode|x86.Build.0 = Release|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|x64.ActiveCfg = Release|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|x64.Build.0 = Release|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|x86.ActiveCfg = Release|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|x86.Build.0 = Release|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|Any CPU.ActiveCfg = WIN32|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|Any CPU.Build.0 = WIN32|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x64.ActiveCfg = Release|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x64.Build.0 = Release|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x86.ActiveCfg = Release|Any CPU
+ {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {2891343C-A29A-4952-B9E1-2468A3DA70DC}
+ EndGlobalSection
+EndGlobal
diff --git a/WhackerLinkConsoleV2/AlertTone.xaml b/DVMConsole/AlertTone.xaml
similarity index 96%
rename from WhackerLinkConsoleV2/AlertTone.xaml
rename to DVMConsole/AlertTone.xaml
index 858d47b..a53cd09 100644
--- a/WhackerLinkConsoleV2/AlertTone.xaml
+++ b/DVMConsole/AlertTone.xaml
@@ -1,4 +1,4 @@
-.
-*
-* Copyright (C) 2025 Caleb, K4PHP
-*
*/
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
-using WhackerLinkLib.Models;
using YamlDotNet.Serialization.NamingConventions;
using YamlDotNet.Serialization;
using System.Diagnostics;
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
public static class AliasTools
{
@@ -55,4 +47,10 @@ namespace WhackerLinkConsoleV2
return match?.Alias ?? string.Empty;
}
}
+
+ public class RadioAlias
+ {
+ public string Alias { get; set; }
+ public int Rid { get; set; }
+ }
}
diff --git a/WhackerLinkConsoleV2/AmbeNative.cs b/DVMConsole/AmbeNative.cs
similarity index 97%
rename from WhackerLinkConsoleV2/AmbeNative.cs
rename to DVMConsole/AmbeNative.cs
index c7ba576..b9575a6 100644
--- a/WhackerLinkConsoleV2/AmbeNative.cs
+++ b/DVMConsole/AmbeNative.cs
@@ -1,12 +1,18 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Threading.Tasks;
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - Audio Bridge
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / Audio Bridge
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
+*
+* Copyright (C) 2023 Bryan Biedenkapp, N2PLL
+*
+*/
#if WIN32
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
///
/// Implements P/Invoke to callback into external AMBE encoder/decoder library.
diff --git a/WhackerLinkConsoleV2/App.xaml b/DVMConsole/App.xaml
similarity index 98%
rename from WhackerLinkConsoleV2/App.xaml
rename to DVMConsole/App.xaml
index 48129b4..6263b34 100644
--- a/WhackerLinkConsoleV2/App.xaml
+++ b/DVMConsole/App.xaml
@@ -1,85 +1,85 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/WhackerLinkConsoleV2/App.xaml.cs b/DVMConsole/App.xaml.cs
similarity index 82%
rename from WhackerLinkConsoleV2/App.xaml.cs
rename to DVMConsole/App.xaml.cs
index 6f584cf..9facf73 100644
--- a/WhackerLinkConsoleV2/App.xaml.cs
+++ b/DVMConsole/App.xaml.cs
@@ -1,14 +1,14 @@
-using System.Configuration;
-using System.Data;
-using System.Windows;
-
-namespace WhackerLinkConsoleV2
-{
- ///
- /// Interaction logic for App.xaml
- ///
- public partial class App : Application
- {
- }
-
-}
+using System.Configuration;
+using System.Data;
+using System.Windows;
+
+namespace DVMConsole
+{
+ ///
+ /// Interaction logic for App.xaml
+ ///
+ public partial class App : Application
+ {
+ }
+
+}
diff --git a/WhackerLinkConsoleV2/AssemblyInfo.cs b/DVMConsole/AssemblyInfo.cs
similarity index 98%
rename from WhackerLinkConsoleV2/AssemblyInfo.cs
rename to DVMConsole/AssemblyInfo.cs
index 372e037..b0ec827 100644
--- a/WhackerLinkConsoleV2/AssemblyInfo.cs
+++ b/DVMConsole/AssemblyInfo.cs
@@ -1,10 +1,10 @@
-using System.Windows;
-
-[assembly: ThemeInfo(
- ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
- //(used if a resource is not found in the page,
- // or application resource dictionaries)
- ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
- //(used if a resource is not found in the page,
- // app, or any theme specific resource dictionaries)
-)]
+using System.Windows;
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
diff --git a/DVMConsole/Assets/DvmLogo.png b/DVMConsole/Assets/DvmLogo.png
new file mode 100644
index 0000000..80c6d54
Binary files /dev/null and b/DVMConsole/Assets/DvmLogo.png differ
diff --git a/WhackerLinkConsoleV2/Assets/WhackerLinkLogoV4.png b/DVMConsole/Assets/WhackerLinkLogoV4.png
similarity index 100%
rename from WhackerLinkConsoleV2/Assets/WhackerLinkLogoV4.png
rename to DVMConsole/Assets/WhackerLinkLogoV4.png
diff --git a/WhackerLinkConsoleV2/Assets/alerttone.png b/DVMConsole/Assets/alerttone.png
similarity index 100%
rename from WhackerLinkConsoleV2/Assets/alerttone.png
rename to DVMConsole/Assets/alerttone.png
diff --git a/WhackerLinkConsoleV2/Assets/alerttone2.png b/DVMConsole/Assets/alerttone2.png
similarity index 100%
rename from WhackerLinkConsoleV2/Assets/alerttone2.png
rename to DVMConsole/Assets/alerttone2.png
diff --git a/WhackerLinkConsoleV2/Assets/audio.png b/DVMConsole/Assets/audio.png
similarity index 100%
rename from WhackerLinkConsoleV2/Assets/audio.png
rename to DVMConsole/Assets/audio.png
diff --git a/WhackerLinkConsoleV2/Assets/channelmarker.png b/DVMConsole/Assets/channelmarker.png
similarity index 100%
rename from WhackerLinkConsoleV2/Assets/channelmarker.png
rename to DVMConsole/Assets/channelmarker.png
diff --git a/WhackerLinkConsoleV2/Assets/clearemerg.png b/DVMConsole/Assets/clearemerg.png
similarity index 100%
rename from WhackerLinkConsoleV2/Assets/clearemerg.png
rename to DVMConsole/Assets/clearemerg.png
diff --git a/WhackerLinkConsoleV2/Assets/config.png b/DVMConsole/Assets/config.png
similarity index 100%
rename from WhackerLinkConsoleV2/Assets/config.png
rename to DVMConsole/Assets/config.png
diff --git a/WhackerLinkConsoleV2/Assets/config2.png b/DVMConsole/Assets/config2.png
similarity index 100%
rename from WhackerLinkConsoleV2/Assets/config2.png
rename to DVMConsole/Assets/config2.png
diff --git a/WhackerLinkConsoleV2/Assets/connection.png b/DVMConsole/Assets/connection.png
similarity index 100%
rename from WhackerLinkConsoleV2/Assets/connection.png
rename to DVMConsole/Assets/connection.png
diff --git a/WhackerLinkConsoleV2/Assets/instantptt.png b/DVMConsole/Assets/instantptt.png
similarity index 100%
rename from WhackerLinkConsoleV2/Assets/instantptt.png
rename to DVMConsole/Assets/instantptt.png
diff --git a/WhackerLinkConsoleV2/Assets/page.png b/DVMConsole/Assets/page.png
similarity index 100%
rename from WhackerLinkConsoleV2/Assets/page.png
rename to DVMConsole/Assets/page.png
diff --git a/WhackerLinkConsoleV2/Assets/pageselect.png b/DVMConsole/Assets/pageselect.png
similarity index 100%
rename from WhackerLinkConsoleV2/Assets/pageselect.png
rename to DVMConsole/Assets/pageselect.png
diff --git a/WhackerLinkConsoleV2/Assets/pttselect.png b/DVMConsole/Assets/pttselect.png
similarity index 100%
rename from WhackerLinkConsoleV2/Assets/pttselect.png
rename to DVMConsole/Assets/pttselect.png
diff --git a/WhackerLinkConsoleV2/Audio/alert1.wav b/DVMConsole/Audio/alert1.wav
similarity index 100%
rename from WhackerLinkConsoleV2/Audio/alert1.wav
rename to DVMConsole/Audio/alert1.wav
diff --git a/WhackerLinkConsoleV2/Audio/alert2.wav b/DVMConsole/Audio/alert2.wav
similarity index 100%
rename from WhackerLinkConsoleV2/Audio/alert2.wav
rename to DVMConsole/Audio/alert2.wav
diff --git a/WhackerLinkConsoleV2/Audio/alert3.wav b/DVMConsole/Audio/alert3.wav
similarity index 100%
rename from WhackerLinkConsoleV2/Audio/alert3.wav
rename to DVMConsole/Audio/alert3.wav
diff --git a/WhackerLinkConsoleV2/Audio/emergency.wav b/DVMConsole/Audio/emergency.wav
similarity index 100%
rename from WhackerLinkConsoleV2/Audio/emergency.wav
rename to DVMConsole/Audio/emergency.wav
diff --git a/WhackerLinkConsoleV2/Audio/hold.wav b/DVMConsole/Audio/hold.wav
similarity index 100%
rename from WhackerLinkConsoleV2/Audio/hold.wav
rename to DVMConsole/Audio/hold.wav
diff --git a/DVMConsole/AudioConverter.cs b/DVMConsole/AudioConverter.cs
new file mode 100644
index 0000000..021fc31
--- /dev/null
+++ b/DVMConsole/AudioConverter.cs
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - DVMConsole
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / DVM Console
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
+*
+* Copyright (C) 2025 Caleb, K4PHP
+*
+*/
+
+namespace DVMConsole
+{
+ ///
+ /// Helper to convert audio between different chunk sizes
+ ///
+ public static class AudioConverter
+ {
+ public const int OriginalPcmLength = 1600;
+ public const int ExpectedPcmLength = 320;
+
+ ///
+ /// Helper to go from a big chunk size to smaller
+ ///
+ ///
+ ///
+ public static List SplitToChunks(byte[] audioData, int OgLength = OriginalPcmLength, int ExepcetedLength = ExpectedPcmLength)
+ {
+ List chunks = new List();
+
+ if (audioData.Length != OgLength)
+ {
+ Console.WriteLine($"Invalid PCM length: {audioData.Length}, expected: {OgLength}");
+ return chunks;
+ }
+
+ for (int offset = 0; offset < OgLength; offset += ExepcetedLength)
+ {
+ byte[] chunk = new byte[ExpectedPcmLength];
+ Buffer.BlockCopy(audioData, offset, chunk, 0, ExepcetedLength);
+ chunks.Add(chunk);
+ }
+
+ return chunks;
+ }
+
+ ///
+ /// Helper to go from small chunks to a big chunk
+ ///
+ ///
+ ///
+ public static byte[] CombineChunks(List chunks, int OgLength = OriginalPcmLength, int ExepcetedLength = ExpectedPcmLength)
+ {
+ if (chunks.Count * ExepcetedLength != OgLength)
+ {
+ Console.WriteLine($"Invalid number of chunks: {chunks.Count}, expected total length: {OgLength}");
+ return null;
+ }
+
+ byte[] combined = new byte[OgLength];
+ int offset = 0;
+
+ foreach (var chunk in chunks)
+ {
+ Buffer.BlockCopy(chunk, 0, combined, offset, ExepcetedLength);
+ offset += ExepcetedLength;
+ }
+
+ return combined;
+ }
+
+ ///
+ /// From https://github.com/W3AXL/rc2-dvm/blob/main/rc2-dvm/Audio.cs
+ ///
+ ///
+ ///
+ public static float[] PcmToFloat(short[] pcm16)
+ {
+ float[] floats = new float[pcm16.Length];
+ for (int i = 0; i < pcm16.Length; i++)
+ {
+ float v = (float)pcm16[i] / (float)short.MaxValue;
+ if (v > 1) { v = 1; }
+ if (v < -1) { v = -1; }
+ floats[i] = v;
+ }
+ return floats;
+ }
+ }
+}
diff --git a/WhackerLinkConsoleV2/AudioManager.cs b/DVMConsole/AudioManager.cs
similarity index 82%
rename from WhackerLinkConsoleV2/AudioManager.cs
rename to DVMConsole/AudioManager.cs
index 7ed87d2..c925342 100644
--- a/WhackerLinkConsoleV2/AudioManager.cs
+++ b/DVMConsole/AudioManager.cs
@@ -1,31 +1,23 @@
-/*
-* WhackerLink - WhackerLinkConsoleV2
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - DVMConsole
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, either version 3 of the License, or
-* (at your option) any later version.
+* @package DVM / DVM Console
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
+* Copyright (C) 2025 Caleb, K4PHP
*
-* You should have received a copy of the GNU General Public License
-* along with this program. If not, see .
-*
-* Copyright (C) 2025 Caleb, K4PHP
-*
*/
using NAudio.Wave;
using NAudio.Wave.SampleProviders;
-using System.Collections.Generic;
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
///
- /// Class for managing
+ /// Class for managing audio streams
///
public class AudioManager
{
diff --git a/WhackerLinkConsoleV2/AudioSettingsWindow.xaml b/DVMConsole/AudioSettingsWindow.xaml
similarity index 95%
rename from WhackerLinkConsoleV2/AudioSettingsWindow.xaml
rename to DVMConsole/AudioSettingsWindow.xaml
index ddcf399..6f5c59e 100644
--- a/WhackerLinkConsoleV2/AudioSettingsWindow.xaml
+++ b/DVMConsole/AudioSettingsWindow.xaml
@@ -1,4 +1,4 @@
-.
-*
-* Copyright (C) 2024-2025 Caleb, K4PHP
-*
*/
using System.Windows;
@@ -23,9 +16,8 @@ using System.Collections.Generic;
using System.Linq;
using NAudio.Wave;
using System.Windows.Controls;
-using WhackerLinkLib.Models.Radio;
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
public partial class AudioSettingsWindow : Window
{
diff --git a/WhackerLinkConsoleV2/CallHistoryWindow.xaml b/DVMConsole/CallHistoryWindow.xaml
similarity index 90%
rename from WhackerLinkConsoleV2/CallHistoryWindow.xaml
rename to DVMConsole/CallHistoryWindow.xaml
index 5c3b10e..e593803 100644
--- a/WhackerLinkConsoleV2/CallHistoryWindow.xaml
+++ b/DVMConsole/CallHistoryWindow.xaml
@@ -1,9 +1,9 @@
-
diff --git a/WhackerLinkConsoleV2/CallHistoryWindow.xaml.cs b/DVMConsole/CallHistoryWindow.xaml.cs
similarity index 87%
rename from WhackerLinkConsoleV2/CallHistoryWindow.xaml.cs
rename to DVMConsole/CallHistoryWindow.xaml.cs
index c8ea69d..e8566a6 100644
--- a/WhackerLinkConsoleV2/CallHistoryWindow.xaml.cs
+++ b/DVMConsole/CallHistoryWindow.xaml.cs
@@ -1,11 +1,21 @@
-using System;
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - DVMConsole
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / DVM Console
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
+*
+* Copyright (C) 2025 Caleb, K4PHP
+*
+*/
+
using System.Collections.ObjectModel;
-using System.Linq;
using System.Windows;
-using System.Windows.Controls;
using System.Windows.Media;
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
public partial class CallHistoryWindow : Window
{
diff --git a/WhackerLinkConsoleV2/ChannelBox.xaml b/DVMConsole/ChannelBox.xaml
similarity index 92%
rename from WhackerLinkConsoleV2/ChannelBox.xaml
rename to DVMConsole/ChannelBox.xaml
index 705ec69..64cb60a 100644
--- a/WhackerLinkConsoleV2/ChannelBox.xaml
+++ b/DVMConsole/ChannelBox.xaml
@@ -1,114 +1,114 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/WhackerLinkConsoleV2/ChannelBox.xaml.cs b/DVMConsole/ChannelBox.xaml.cs
similarity index 90%
rename from WhackerLinkConsoleV2/ChannelBox.xaml.cs
rename to DVMConsole/ChannelBox.xaml.cs
index 18c9d2d..796f686 100644
--- a/WhackerLinkConsoleV2/ChannelBox.xaml.cs
+++ b/DVMConsole/ChannelBox.xaml.cs
@@ -1,340 +1,333 @@
-/*
-* WhackerLink - WhackerLinkConsoleV2
-*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, either version 3 of the License, or
-* (at your option) any later version.
-*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
-*
-* You should have received a copy of the GNU General Public License
-* along with this program. If not, see .
-*
-* Copyright (C) 2024-2025 Caleb, K4PHP
-*
-*/
-
-using System.ComponentModel;
-using System.Security.Cryptography;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Input;
-using System.Windows.Media;
-using fnecore.P25;
-
-namespace WhackerLinkConsoleV2.Controls
-{
- public partial class ChannelBox : UserControl, INotifyPropertyChanged
- {
- private readonly SelectedChannelsManager _selectedChannelsManager;
- private readonly AudioManager _audioManager;
-
- private bool _pttState;
- private bool _pageState;
- private bool _holdState;
- private bool _emergency;
- private string _lastSrcId = "0";
- private double _volume = 1.0;
-
- internal LinearGradientBrush grayGradient;
- internal LinearGradientBrush redGradient;
- internal LinearGradientBrush orangeGradient;
-
- public FlashingBackgroundManager _flashingBackgroundManager;
-
- public event EventHandler PTTButtonClicked;
- public event EventHandler PageButtonClicked;
- public event EventHandler HoldChannelButtonClicked;
-
- public event PropertyChangedEventHandler PropertyChanged;
-
- public byte[] netLDU1 = new byte[9 * 25];
- public byte[] netLDU2 = new byte[9 * 25];
-
- public int p25N { get; set; } = 0;
- public int p25SeqNo { get; set; } = 0;
- public int p25Errs { get; set; } = 0;
-
- public byte[] mi = new byte[P25Defines.P25_MI_LENGTH]; // Message Indicator
- public byte algId = 0; // Algorithm ID
- public ushort kId = 0; // Key ID
-
- public List chunkedPcm = new List();
-
- public string ChannelName { get; set; }
- public string SystemName { get; set; }
- public string DstId { get; set; }
-
-#if WIN32
- public AmbeVocoder extFullRateVocoder;
- public AmbeVocoder extHalfRateVocoder;
-#endif
- public MBEEncoder encoder;
- public MBEDecoder decoder;
-
- public MBEToneDetector toneDetector = new MBEToneDetector();
-
- public P25Crypto crypter = new P25Crypto();
-
- public bool IsReceiving { get; set; } = false;
- public bool IsReceivingEncrypted { get; set; } = false;
-
- public string LastSrcId
- {
- get => _lastSrcId;
- set
- {
- if (_lastSrcId != value)
- {
- _lastSrcId = value;
- OnPropertyChanged(nameof(LastSrcId));
- }
- }
- }
-
- public bool PttState
- {
- get => _pttState;
- set
- {
- _pttState = value;
- UpdatePTTColor();
- }
- }
-
- public bool PageState
- {
- get => _pageState;
- set
- {
- _pageState = value;
- UpdatePageColor();
- }
- }
-
- public bool HoldState
- {
- get => _holdState;
- set
- {
- _holdState = value;
- UpdateHoldColor();
- }
- }
-
- public bool Emergency
- {
- get => _emergency;
- set
- {
- _emergency = value;
-
- Dispatcher.Invoke(() =>
- {
- if (value)
- _flashingBackgroundManager.Start();
- else
- _flashingBackgroundManager.Stop();
- });
- }
- }
-
- public string VoiceChannel { get; set; }
-
- public bool IsEditMode { get; set; }
-
- private bool _isSelected;
- public bool IsSelected
- {
- get => _isSelected;
- set
- {
- _isSelected = value;
- UpdateBackground();
- }
- }
-
- public double Volume
- {
- get => _volume;
- set
- {
- if (_volume != value)
- {
- _volume = value;
- OnPropertyChanged(nameof(Volume));
- _audioManager.SetTalkgroupVolume(DstId, (float)value);
- }
- }
- }
-
- public uint txStreamId { get; internal set; }
-
- public ChannelBox(SelectedChannelsManager selectedChannelsManager, AudioManager audioManager, string channelName, string systemName, string dstId)
- {
- InitializeComponent();
- DataContext = this;
- _selectedChannelsManager = selectedChannelsManager;
- _audioManager = audioManager;
- _flashingBackgroundManager = new FlashingBackgroundManager(this);
- ChannelName = channelName;
- DstId = dstId;
- SystemName = $"System: {systemName}";
- LastSrcId = $"Last SRC: {LastSrcId}";
- UpdateBackground();
- MouseLeftButtonDown += ChannelBox_MouseLeftButtonDown;
-
- grayGradient = new LinearGradientBrush
- {
- StartPoint = new Point(0.5, 0),
- EndPoint = new Point(0.5, 1)
- };
-
- grayGradient.GradientStops.Add(new GradientStop((Color)ColorConverter.ConvertFromString("#FFF0F0F0"), 0.485));
- grayGradient.GradientStops.Add(new GradientStop((Color)ColorConverter.ConvertFromString("#FFDCDCDC"), 0.517));
-
- redGradient = new LinearGradientBrush
- {
- StartPoint = new Point(0.5, 0),
- EndPoint = new Point(0.5, 1)
- };
-
- redGradient.GradientStops.Add(new GradientStop((Color)ColorConverter.ConvertFromString("#FFFF0000"), 0.485));
- redGradient.GradientStops.Add(new GradientStop((Color)ColorConverter.ConvertFromString("#FFD50000"), 0.517));
-
- orangeGradient = new LinearGradientBrush
- {
- StartPoint = new Point(0.5, 0),
- EndPoint = new Point(0.5, 1)
- };
-
- orangeGradient.GradientStops.Add(new GradientStop((Color)ColorConverter.ConvertFromString("#FFFFAF00"), 0.485));
- orangeGradient.GradientStops.Add(new GradientStop((Color)ColorConverter.ConvertFromString("#FFEEA400"), 0.517));
-
- PttButton.Background = grayGradient;
- PageSelectButton.Background = grayGradient;
- ChannelMarkerBtn.Background = grayGradient;
-
- if (SystemName == MainWindow.PLAYBACKSYS || ChannelName == MainWindow.PLAYBACKCHNAME || DstId == MainWindow.PLAYBACKTG)
- {
- PttButton.IsEnabled = false;
- PageSelectButton.IsEnabled = false;
- ChannelMarkerBtn.IsEnabled = false;
- }
- }
-
- private void ChannelBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- if (IsEditMode) return;
-
- IsSelected = !IsSelected;
- Background = IsSelected ? (Brush)new BrushConverter().ConvertFrom("#FF0B004B") : Brushes.Gray;
-
- if (IsSelected)
- {
- _selectedChannelsManager.AddSelectedChannel(this);
- }
- else
- {
- _selectedChannelsManager.RemoveSelectedChannel(this);
- }
- }
-
- private void UpdatePTTColor()
- {
- if (IsEditMode) return;
-
- if (PttState)
- PttButton.Background = redGradient;
- else
- PttButton.Background = grayGradient;
- }
-
- private void UpdatePageColor()
- {
- if (IsEditMode) return;
-
- if (PageState)
- PageSelectButton.Background = orangeGradient;
- else
- PageSelectButton.Background = grayGradient;
- }
-
- private void UpdateHoldColor()
- {
- if (IsEditMode) return;
-
- if (HoldState)
- ChannelMarkerBtn.Background = orangeGradient;
- else
- ChannelMarkerBtn.Background = grayGradient;
- }
-
- private void UpdateBackground()
- {
- if (SystemName == MainWindow.PLAYBACKSYS || ChannelName == MainWindow.PLAYBACKCHNAME || DstId == MainWindow.PLAYBACKTG)
- {
- Background = IsSelected ? (Brush)new BrushConverter().ConvertFrom("#FFC90000") : Brushes.DarkGray;
- return;
- }
-
- Background = IsSelected ? (Brush)new BrushConverter().ConvertFrom("#FF0B004B") : Brushes.DarkGray;
- }
-
- private async void PTTButton_Click(object sender, RoutedEventArgs e)
- {
- if (!IsSelected) return;
-
- if (PttState)
- await Task.Delay(500);
-
- PttState = !PttState;
-
- PTTButtonClicked.Invoke(sender, this);
- }
-
- private void PageSelectButton_Click(object sender, RoutedEventArgs e)
- {
- if (!IsSelected) return;
-
- PageState = !PageState;
- PageButtonClicked.Invoke(sender, this);
- }
-
- private void VolumeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
- {
- Volume = e.NewValue;
- }
-
- protected virtual void OnPropertyChanged(string propertyName)
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
-
- private void ChannelMarkerBtn_Click(object sender, RoutedEventArgs e)
- {
- if (!IsSelected) return;
-
- HoldState = !HoldState;
- HoldChannelButtonClicked.Invoke(sender, this);
- }
-
- private void PttButton_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
- {
- if (!IsSelected || PttState) return;
-
- ((Button)sender).Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FF3FA0FF"));
- }
-
- private void PttButton_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
- {
- if (!IsSelected || PttState) return;
-
- ((Button)sender).Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FFDDDDDD"));
- }
- }
-}
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - DVMConsole
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / DVM Console
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
+*
+* Copyright (C) 2025 Caleb, K4PHP
+*
+*/
+
+using System.ComponentModel;
+using System.Security.Cryptography;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+using fnecore.P25;
+
+namespace DVMConsole.Controls
+{
+ public partial class ChannelBox : UserControl, INotifyPropertyChanged
+ {
+ private readonly SelectedChannelsManager _selectedChannelsManager;
+ private readonly AudioManager _audioManager;
+
+ private bool _pttState;
+ private bool _pageState;
+ private bool _holdState;
+ private bool _emergency;
+ private string _lastSrcId = "0";
+ private double _volume = 1.0;
+
+ internal LinearGradientBrush grayGradient;
+ internal LinearGradientBrush redGradient;
+ internal LinearGradientBrush orangeGradient;
+
+ public FlashingBackgroundManager _flashingBackgroundManager;
+
+ public event EventHandler PTTButtonClicked;
+ public event EventHandler PageButtonClicked;
+ public event EventHandler HoldChannelButtonClicked;
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public byte[] netLDU1 = new byte[9 * 25];
+ public byte[] netLDU2 = new byte[9 * 25];
+
+ public int p25N { get; set; } = 0;
+ public int p25SeqNo { get; set; } = 0;
+ public int p25Errs { get; set; } = 0;
+
+ public byte[] mi = new byte[P25Defines.P25_MI_LENGTH]; // Message Indicator
+ public byte algId = 0; // Algorithm ID
+ public ushort kId = 0; // Key ID
+
+ public List chunkedPcm = new List();
+
+ public string ChannelName { get; set; }
+ public string SystemName { get; set; }
+ public string DstId { get; set; }
+
+#if WIN32
+ public AmbeVocoder extFullRateVocoder;
+ public AmbeVocoder extHalfRateVocoder;
+#endif
+ public MBEEncoder encoder;
+ public MBEDecoder decoder;
+
+ public MBEToneDetector toneDetector = new MBEToneDetector();
+
+ public P25Crypto crypter = new P25Crypto();
+
+ public bool IsReceiving { get; set; } = false;
+ public bool IsReceivingEncrypted { get; set; } = false;
+
+ public string LastSrcId
+ {
+ get => _lastSrcId;
+ set
+ {
+ if (_lastSrcId != value)
+ {
+ _lastSrcId = value;
+ OnPropertyChanged(nameof(LastSrcId));
+ }
+ }
+ }
+
+ public bool PttState
+ {
+ get => _pttState;
+ set
+ {
+ _pttState = value;
+ UpdatePTTColor();
+ }
+ }
+
+ public bool PageState
+ {
+ get => _pageState;
+ set
+ {
+ _pageState = value;
+ UpdatePageColor();
+ }
+ }
+
+ public bool HoldState
+ {
+ get => _holdState;
+ set
+ {
+ _holdState = value;
+ UpdateHoldColor();
+ }
+ }
+
+ public bool Emergency
+ {
+ get => _emergency;
+ set
+ {
+ _emergency = value;
+
+ Dispatcher.Invoke(() =>
+ {
+ if (value)
+ _flashingBackgroundManager.Start();
+ else
+ _flashingBackgroundManager.Stop();
+ });
+ }
+ }
+
+ public string VoiceChannel { get; set; }
+
+ public bool IsEditMode { get; set; }
+
+ private bool _isSelected;
+ public bool IsSelected
+ {
+ get => _isSelected;
+ set
+ {
+ _isSelected = value;
+ UpdateBackground();
+ }
+ }
+
+ public double Volume
+ {
+ get => _volume;
+ set
+ {
+ if (_volume != value)
+ {
+ _volume = value;
+ OnPropertyChanged(nameof(Volume));
+ _audioManager.SetTalkgroupVolume(DstId, (float)value);
+ }
+ }
+ }
+
+ public uint txStreamId { get; internal set; }
+
+ public ChannelBox(SelectedChannelsManager selectedChannelsManager, AudioManager audioManager, string channelName, string systemName, string dstId)
+ {
+ InitializeComponent();
+ DataContext = this;
+ _selectedChannelsManager = selectedChannelsManager;
+ _audioManager = audioManager;
+ _flashingBackgroundManager = new FlashingBackgroundManager(this);
+ ChannelName = channelName;
+ DstId = dstId;
+ SystemName = $"System: {systemName}";
+ LastSrcId = $"Last SRC: {LastSrcId}";
+ UpdateBackground();
+ MouseLeftButtonDown += ChannelBox_MouseLeftButtonDown;
+
+ grayGradient = new LinearGradientBrush
+ {
+ StartPoint = new Point(0.5, 0),
+ EndPoint = new Point(0.5, 1)
+ };
+
+ grayGradient.GradientStops.Add(new GradientStop((Color)ColorConverter.ConvertFromString("#FFF0F0F0"), 0.485));
+ grayGradient.GradientStops.Add(new GradientStop((Color)ColorConverter.ConvertFromString("#FFDCDCDC"), 0.517));
+
+ redGradient = new LinearGradientBrush
+ {
+ StartPoint = new Point(0.5, 0),
+ EndPoint = new Point(0.5, 1)
+ };
+
+ redGradient.GradientStops.Add(new GradientStop((Color)ColorConverter.ConvertFromString("#FFFF0000"), 0.485));
+ redGradient.GradientStops.Add(new GradientStop((Color)ColorConverter.ConvertFromString("#FFD50000"), 0.517));
+
+ orangeGradient = new LinearGradientBrush
+ {
+ StartPoint = new Point(0.5, 0),
+ EndPoint = new Point(0.5, 1)
+ };
+
+ orangeGradient.GradientStops.Add(new GradientStop((Color)ColorConverter.ConvertFromString("#FFFFAF00"), 0.485));
+ orangeGradient.GradientStops.Add(new GradientStop((Color)ColorConverter.ConvertFromString("#FFEEA400"), 0.517));
+
+ PttButton.Background = grayGradient;
+ PageSelectButton.Background = grayGradient;
+ ChannelMarkerBtn.Background = grayGradient;
+
+ if (SystemName == MainWindow.PLAYBACKSYS || ChannelName == MainWindow.PLAYBACKCHNAME || DstId == MainWindow.PLAYBACKTG)
+ {
+ PttButton.IsEnabled = false;
+ PageSelectButton.IsEnabled = false;
+ ChannelMarkerBtn.IsEnabled = false;
+ }
+ }
+
+ private void ChannelBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ if (IsEditMode) return;
+
+ IsSelected = !IsSelected;
+ Background = IsSelected ? (Brush)new BrushConverter().ConvertFrom("#FF0B004B") : Brushes.Gray;
+
+ if (IsSelected)
+ {
+ _selectedChannelsManager.AddSelectedChannel(this);
+ }
+ else
+ {
+ _selectedChannelsManager.RemoveSelectedChannel(this);
+ }
+ }
+
+ private void UpdatePTTColor()
+ {
+ if (IsEditMode) return;
+
+ if (PttState)
+ PttButton.Background = redGradient;
+ else
+ PttButton.Background = grayGradient;
+ }
+
+ private void UpdatePageColor()
+ {
+ if (IsEditMode) return;
+
+ if (PageState)
+ PageSelectButton.Background = orangeGradient;
+ else
+ PageSelectButton.Background = grayGradient;
+ }
+
+ private void UpdateHoldColor()
+ {
+ if (IsEditMode) return;
+
+ if (HoldState)
+ ChannelMarkerBtn.Background = orangeGradient;
+ else
+ ChannelMarkerBtn.Background = grayGradient;
+ }
+
+ private void UpdateBackground()
+ {
+ if (SystemName == MainWindow.PLAYBACKSYS || ChannelName == MainWindow.PLAYBACKCHNAME || DstId == MainWindow.PLAYBACKTG)
+ {
+ Background = IsSelected ? (Brush)new BrushConverter().ConvertFrom("#FFC90000") : Brushes.DarkGray;
+ return;
+ }
+
+ Background = IsSelected ? (Brush)new BrushConverter().ConvertFrom("#FF0B004B") : Brushes.DarkGray;
+ }
+
+ private async void PTTButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (!IsSelected) return;
+
+ if (PttState)
+ await Task.Delay(500);
+
+ PttState = !PttState;
+
+ PTTButtonClicked.Invoke(sender, this);
+ }
+
+ private void PageSelectButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (!IsSelected) return;
+
+ PageState = !PageState;
+ PageButtonClicked.Invoke(sender, this);
+ }
+
+ private void VolumeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs e)
+ {
+ Volume = e.NewValue;
+ }
+
+ protected virtual void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ private void ChannelMarkerBtn_Click(object sender, RoutedEventArgs e)
+ {
+ if (!IsSelected) return;
+
+ HoldState = !HoldState;
+ HoldChannelButtonClicked.Invoke(sender, this);
+ }
+
+ private void PttButton_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
+ {
+ if (!IsSelected || PttState) return;
+
+ ((Button)sender).Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FF3FA0FF"));
+ }
+
+ private void PttButton_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
+ {
+ if (!IsSelected || PttState) return;
+
+ ((Button)sender).Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FFDDDDDD"));
+ }
+ }
+}
diff --git a/DVMConsole/ChannelPosition.cs b/DVMConsole/ChannelPosition.cs
new file mode 100644
index 0000000..3c3ff9e
--- /dev/null
+++ b/DVMConsole/ChannelPosition.cs
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - DVMConsole
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / DVM Console
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
+*
+* Copyright (C) 2025 Caleb, K4PHP
+*
+*/
+
+namespace DVMConsole
+{
+ public class ChannelPosition
+ {
+ public double X { get; set; }
+ public double Y { get; set; }
+ }
+}
diff --git a/DVMConsole/Codeplug.cs b/DVMConsole/Codeplug.cs
new file mode 100644
index 0000000..8748a38
--- /dev/null
+++ b/DVMConsole/Codeplug.cs
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - DVMConsole
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / DVM Console
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
+*
+* Copyright (C) 2024-2025 Caleb, K4PHP
+*
+*/
+
+using System.Security.Policy;
+
+namespace DVMConsole
+{
+
+ ///
+ /// Codeplug object used project wide
+ ///
+ public class Codeplug
+ {
+ public List Systems { get; set; }
+ public List Zones { get; set; }
+
+ ///
+ ///
+ ///
+ public class System
+ {
+ public string Name { get; set; }
+ public string Address { get; set; }
+ public uint PeerId { get; set; }
+ public int Port { get; set; }
+ public string Rid { get; set; }
+ public string AuthKey { get; set; }
+ public string AliasPath { get; set; } = "./alias.yml";
+ public List RidAlias { get; set; } = null;
+
+ public override string ToString()
+ {
+ return Name;
+ }
+ }
+
+ ///
+ ///
+ ///
+ public class Zone
+ {
+ public string Name { get; set; }
+ public List Channels { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public class Channel
+ {
+ public string Name { get; set; }
+ public string System { get; set; }
+ public string Tgid { get; set; }
+ public string EncryptionKey { get; set; }
+ public string AlgoId { get; set; } = "0x80";
+ public string KeyId { get; set; }
+
+ public ushort GetKeyId()
+ {
+ return Convert.ToUInt16(KeyId, 16);
+ }
+
+ public byte GetAlgoId()
+ {
+ return Convert.ToByte(AlgoId, 16);
+ }
+
+ public byte[] GetEncryptionKey()
+ {
+ if (EncryptionKey == null)
+ return [];
+
+ return EncryptionKey
+ .Split(',')
+ .Select(s => Convert.ToByte(s.Trim(), 16))
+ .ToArray();
+ }
+ }
+
+ ///
+ /// Helper to return a system by looking up a
+ ///
+ ///
+ ///
+ public System GetSystemForChannel(Channel channel)
+ {
+ return Systems.FirstOrDefault(s => s.Name == channel.System);
+ }
+
+ ///
+ /// Helper to return a system by looking up a channel name
+ ///
+ ///
+ ///
+ public System GetSystemForChannel(string channelName)
+ {
+ foreach (var zone in Zones)
+ {
+ var channel = zone.Channels.FirstOrDefault(c => c.Name == channelName);
+ if (channel != null)
+ {
+ return Systems.FirstOrDefault(s => s.Name == channel.System);
+ }
+ }
+ return null;
+ }
+
+ ///
+ /// Helper to return a by channel name
+ ///
+ ///
+ ///
+ public Channel GetChannelByName(string channelName)
+ {
+ foreach (var zone in Zones)
+ {
+ var channel = zone.Channels.FirstOrDefault(c => c.Name == channelName);
+ if (channel != null)
+ {
+ return channel;
+ }
+ }
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/DVMConsole/ConsoleNative.cs b/DVMConsole/ConsoleNative.cs
new file mode 100644
index 0000000..a91db88
--- /dev/null
+++ b/DVMConsole/ConsoleNative.cs
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - DVMConsole
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / DVM Console
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
+*
+* Copyright (C) 2025 Caleb, K4PHP
+*
+*/
+
+using System.Runtime.InteropServices;
+
+namespace DVMConsole
+{
+ public static class ConsoleNative
+ {
+ [DllImport("kernel32.dll")]
+ private static extern bool AllocConsole();
+
+ public static void ShowConsole()
+ {
+ AllocConsole();
+ Console.WriteLine("Console attached.");
+ }
+ }
+}
diff --git a/WhackerLinkConsoleV2/WhackerLinkConsoleV2.csproj b/DVMConsole/DVMConsole.csproj
similarity index 57%
rename from WhackerLinkConsoleV2/WhackerLinkConsoleV2.csproj
rename to DVMConsole/DVMConsole.csproj
index fc2ff0f..5c115e0 100644
--- a/WhackerLinkConsoleV2/WhackerLinkConsoleV2.csproj
+++ b/DVMConsole/DVMConsole.csproj
@@ -1,102 +1,131 @@
-
-
-
- WinExe
- net8.0-windows7.0
- disable
- enable
- true
- AnyCPU;x64;x86
- Debug;Release;WIN32
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Always
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
- Always
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ WinExe
+ net8.0-windows7.0
+ disable
+ enable
+ true
+ AnyCPU;x64;x86
+ Debug;Release;WIN32
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+
+
+
+ Always
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/WhackerLinkConsoleV2/DigitalPageWindow.xaml b/DVMConsole/DigitalPageWindow.xaml
similarity index 94%
rename from WhackerLinkConsoleV2/DigitalPageWindow.xaml
rename to DVMConsole/DigitalPageWindow.xaml
index 539438d..5ba7268 100644
--- a/WhackerLinkConsoleV2/DigitalPageWindow.xaml
+++ b/DVMConsole/DigitalPageWindow.xaml
@@ -1,4 +1,4 @@
-.
-*
-* Copyright (C) 2024 Caleb, K4PHP
-*
*/
using System.Windows;
-using WhackerLinkLib.Models.Radio;
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
///
/// Interaction logic for DigitalPageWindow.xaml
diff --git a/WhackerLinkConsoleV2/FlashingBackgroundManager.cs b/DVMConsole/FlashingBackgroundManager.cs
similarity index 82%
rename from WhackerLinkConsoleV2/FlashingBackgroundManager.cs
rename to DVMConsole/FlashingBackgroundManager.cs
index 3190ebe..6a66542 100644
--- a/WhackerLinkConsoleV2/FlashingBackgroundManager.cs
+++ b/DVMConsole/FlashingBackgroundManager.cs
@@ -1,21 +1,14 @@
-/*
-* WhackerLink - WhackerLinkConsoleV2
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - DVMConsole
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, either version 3 of the License, or
-* (at your option) any later version.
+* @package DVM / DVM Console
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
+* Copyright (C) 2025 Caleb, K4PHP
*
-* You should have received a copy of the GNU General Public License
-* along with this program. If not, see .
-*
-* Copyright (C) 2024 Caleb, K4PHP
-*
*/
using System.Windows;
@@ -23,7 +16,7 @@ using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Threading;
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
public class FlashingBackgroundManager
{
diff --git a/WhackerLinkConsoleV2/FneSystemBase.DMR.cs b/DVMConsole/FneSystemBase.DMR.cs
similarity index 90%
rename from WhackerLinkConsoleV2/FneSystemBase.DMR.cs
rename to DVMConsole/FneSystemBase.DMR.cs
index 00f5da6..1a85812 100644
--- a/WhackerLinkConsoleV2/FneSystemBase.DMR.cs
+++ b/DVMConsole/FneSystemBase.DMR.cs
@@ -1,19 +1,22 @@
-using Microsoft.VisualBasic;
-using Serilog;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.Sockets;
-using System.Net;
-using System.Text;
-using System.Threading.Tasks;
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - Audio Bridge
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / Audio Bridge
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
+*
+* Copyright (C) 2022-2024 Bryan Biedenkapp, N2PLL
+*
+*/
using fnecore.DMR;
using fnecore;
using NAudio.Wave;
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
///
/// Implements a FNE system base.
diff --git a/WhackerLinkConsoleV2/FneSystemBase.NXDN.cs b/DVMConsole/FneSystemBase.NXDN.cs
similarity index 80%
rename from WhackerLinkConsoleV2/FneSystemBase.NXDN.cs
rename to DVMConsole/FneSystemBase.NXDN.cs
index 9cc7c86..99301d9 100644
--- a/WhackerLinkConsoleV2/FneSystemBase.NXDN.cs
+++ b/DVMConsole/FneSystemBase.NXDN.cs
@@ -1,12 +1,20 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - Audio Bridge
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / Audio Bridge
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
+*
+* Copyright (C) 2022-2024 Bryan Biedenkapp, N2PLL
+*
+*/
+
using fnecore.NXDN;
using fnecore;
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
///
/// Implements a FNE system base.
diff --git a/WhackerLinkConsoleV2/FneSystemBase.P25.cs b/DVMConsole/FneSystemBase.P25.cs
similarity index 97%
rename from WhackerLinkConsoleV2/FneSystemBase.P25.cs
rename to DVMConsole/FneSystemBase.P25.cs
index e2ab506..67e4fe8 100644
--- a/WhackerLinkConsoleV2/FneSystemBase.P25.cs
+++ b/DVMConsole/FneSystemBase.P25.cs
@@ -8,25 +8,14 @@
* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
* Copyright (C) 2022-2024 Bryan Biedenkapp, N2PLL
+* Copyright (C) 2025 Caleb, K4PHP
*
*/
-using System;
-using System.Collections.Generic;
-using System.Net;
-using System.Net.Sockets;
-using System.Threading.Tasks;
-
-using Serilog;
using fnecore;
using fnecore.P25;
-using NAudio.Wave;
-using System.Windows.Threading;
-using System.Security.Cryptography;
-using System.Security.Cryptography.Xml;
-
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
///
/// Implements a FNE system base.
@@ -340,7 +329,7 @@ namespace WhackerLinkConsoleV2
break;
case P25DFSI.P25_DFSI_LDU2_VOICE12:
{
- dfsiFrame[1U] = cryptoParams.Mi[0]; // Message Indicator
+ dfsiFrame[1U] = cryptoParams.Mi[0]; // Message Indicator
dfsiFrame[2U] = cryptoParams.Mi[1];
dfsiFrame[3U] = cryptoParams.Mi[2];
Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE
@@ -348,7 +337,7 @@ namespace WhackerLinkConsoleV2
break;
case P25DFSI.P25_DFSI_LDU2_VOICE13:
{
- dfsiFrame[1U] = cryptoParams.Mi[3]; // Message Indicator
+ dfsiFrame[1U] = cryptoParams.Mi[3]; // Message Indicator
dfsiFrame[2U] = cryptoParams.Mi[4];
dfsiFrame[3U] = cryptoParams.Mi[5];
Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE
@@ -356,7 +345,7 @@ namespace WhackerLinkConsoleV2
break;
case P25DFSI.P25_DFSI_LDU2_VOICE14:
{
- dfsiFrame[1U] = cryptoParams.Mi[6]; // Message Indicator
+ dfsiFrame[1U] = cryptoParams.Mi[6]; // Message Indicator
dfsiFrame[2U] = cryptoParams.Mi[7];
dfsiFrame[3U] = cryptoParams.Mi[8];
Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE
@@ -364,9 +353,8 @@ namespace WhackerLinkConsoleV2
break;
case P25DFSI.P25_DFSI_LDU2_VOICE15:
{
- dfsiFrame[1U] = cryptoParams.AlgId; // Algorithm ID
- FneUtils.WriteBytes(cryptoParams.KeyId, ref dfsiFrame, 2); // Key ID
- //dfsiFrame[3U] = 0;
+ dfsiFrame[1U] = cryptoParams.AlgId; // Algorithm ID
+ FneUtils.WriteBytes(cryptoParams.KeyId, ref dfsiFrame, 2); // Key ID
Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE
}
break;
diff --git a/WhackerLinkConsoleV2/FneSystemBase.cs b/DVMConsole/FneSystemBase.cs
similarity index 93%
rename from WhackerLinkConsoleV2/FneSystemBase.cs
rename to DVMConsole/FneSystemBase.cs
index c101167..f3ea2f0 100644
--- a/WhackerLinkConsoleV2/FneSystemBase.cs
+++ b/DVMConsole/FneSystemBase.cs
@@ -1,13 +1,22 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - DVMConsole
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / DVM Console
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
+*
+* Copyright (C) 2022-2024 Bryan Biedenkapp, N2PLL
+* Copyright (C) 2025 Caleb, K4PHP
+*
+*/
+
using fnecore.DMR;
using fnecore;
using fnecore.P25.kmm;
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
///
/// Represents the individual timeslot data status.
@@ -97,14 +106,10 @@ namespace WhackerLinkConsoleV2
///
public abstract partial class FneSystemBase : fnecore.FneSystemBase
{
- public List processedChunks = new List();
-
private Random rand;
-
internal MainWindow mainWindow;
- // List of active calls
- private List<(uint, byte)> activeTalkgroups = new List<(uint, byte)>();
+ public List processedChunks = new List();
/*
** Methods
diff --git a/WhackerLinkConsoleV2/FneSystemManager.cs b/DVMConsole/FneSystemManager.cs
similarity index 75%
rename from WhackerLinkConsoleV2/FneSystemManager.cs
rename to DVMConsole/FneSystemManager.cs
index f0f7e9d..356aa89 100644
--- a/WhackerLinkConsoleV2/FneSystemManager.cs
+++ b/DVMConsole/FneSystemManager.cs
@@ -1,27 +1,17 @@
-/*
-* WhackerLink - WhackerLinkLib
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - DVMConsole
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, either version 3 of the License, or
-* (at your option) any later version.
+* @package DVM / DVM Console
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
+* Copyright (C) 2025 Caleb, K4PHP
*
-* You should have received a copy of the GNU General Public License
-* along with this program. If not, see .
-*
-* Copyright (C) 2024-2025 Caleb, K4PHP
-*
*/
-using WhackerLinkLib.Models.Radio;
-using WhackerLinkLib.Network;
-
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
///
/// WhackerLink peer/client websocket manager for having multiple systems
diff --git a/WhackerLinkConsoleV2/GainSampleProvider.cs b/DVMConsole/GainSampleProvider.cs
similarity index 55%
rename from WhackerLinkConsoleV2/GainSampleProvider.cs
rename to DVMConsole/GainSampleProvider.cs
index eef28bf..06dfff4 100644
--- a/WhackerLinkConsoleV2/GainSampleProvider.cs
+++ b/DVMConsole/GainSampleProvider.cs
@@ -1,28 +1,21 @@
-/*
-* WhackerLink - WhackerLinkConsoleV2
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - DVMConsole
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, either version 3 of the License, or
-* (at your option) any later version.
+* @package DVM / DVM Console
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
+* Copyright (C) 2025 Caleb, K4PHP
*
-* You should have received a copy of the GNU General Public License
-* along with this program. If not, see .
-*
-* Copyright (C) 2025 Caleb, K4PHP
-*
*/
using NAudio.Wave;
using NAudio.Wave.SampleProviders;
using System;
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
public class GainSampleProvider : ISampleProvider
{
diff --git a/WhackerLinkConsoleV2/KeyStatusWindow.xaml b/DVMConsole/KeyStatusWindow.xaml
similarity index 94%
rename from WhackerLinkConsoleV2/KeyStatusWindow.xaml
rename to DVMConsole/KeyStatusWindow.xaml
index 6ed6341..fa9f55f 100644
--- a/WhackerLinkConsoleV2/KeyStatusWindow.xaml
+++ b/DVMConsole/KeyStatusWindow.xaml
@@ -1,4 +1,4 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/WhackerLinkConsoleV2/MainWindow.xaml.cs b/DVMConsole/MainWindow.xaml.cs
similarity index 64%
rename from WhackerLinkConsoleV2/MainWindow.xaml.cs
rename to DVMConsole/MainWindow.xaml.cs
index eabe7be..7a70298 100644
--- a/WhackerLinkConsoleV2/MainWindow.xaml.cs
+++ b/DVMConsole/MainWindow.xaml.cs
@@ -1,2260 +1,1620 @@
-/*
-* WhackerLink - WhackerLinkConsoleV2
-*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, either version 3 of the License, or
-* (at your option) any later version.
-*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
-*
-* You should have received a copy of the GNU General Public License
-* along with this program. If not, see .
-*
-* Copyright (C) 2024-2025 Caleb, K4PHP
-* Copyright (C) 2025 J. Dean
-*
-*/
-
-using Microsoft.Win32;
-using System;
-using System.Timers;
-using System.IO;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Input;
-using WhackerLinkLib.Models.Radio;
-using YamlDotNet.Serialization;
-using YamlDotNet.Serialization.NamingConventions;
-using WhackerLinkConsoleV2.Controls;
-using WebSocketManager = WhackerLinkLib.Managers.WebSocketManager;
-using System.Windows.Media;
-using WhackerLinkLib.Utils;
-using WhackerLinkLib.Models;
-using System.Net;
-using NAudio.Wave;
-using WhackerLinkLib.Interfaces;
-using WhackerLinkLib.Models.IOSP;
-using fnecore.P25;
-using fnecore;
-using Microsoft.VisualBasic;
-using System.Text;
-using Nancy;
-using Constants = fnecore.Constants;
-using System.Security.Cryptography;
-using fnecore.P25.LC.TSBK;
-using WebSocketSharp;
-using NWaves.Signals;
-using static WhackerLinkConsoleV2.P25Crypto;
-using static WhackerLinkLib.Models.Radio.Codeplug;
-
-namespace WhackerLinkConsoleV2
-{
- public partial class MainWindow : Window
- {
- public Codeplug Codeplug { get; set; }
- private bool isEditMode = false;
-
- private bool globalPttState = false;
-
- private UIElement _draggedElement;
- private Point _startPoint;
- private double _offsetX;
- private double _offsetY;
- private bool _isDragging;
-
- private SettingsManager _settingsManager = new SettingsManager();
- private SelectedChannelsManager _selectedChannelsManager;
- private FlashingBackgroundManager _flashingManager;
- private WaveFilePlaybackManager _emergencyAlertPlayback;
- private WebSocketManager _webSocketManager = new WebSocketManager();
-
- private ChannelBox playbackChannelBox;
-
- CallHistoryWindow callHistoryWindow = new CallHistoryWindow();
-
- public static string PLAYBACKTG = "LOCPLAYBACK";
- public static string PLAYBACKSYS = "LOCPLAYBACKSYS";
- public static string PLAYBACKCHNAME = "PLAYBACK";
-
- private readonly WaveInEvent _waveIn;
- private readonly AudioManager _audioManager;
-
- private static System.Timers.Timer _channelHoldTimer;
-
- private Dictionary systemStatuses = new Dictionary();
- private FneSystemManager _fneSystemManager = new FneSystemManager();
-
- private bool cryptodev = true;
-
- private static HashSet usedRids = new HashSet();
-
- List> fneAffs = new List>();
-
- public MainWindow()
- {
-#if !DEBUG
- ConsoleNative.ShowConsole();
-#endif
- InitializeComponent();
- _settingsManager.LoadSettings();
- _selectedChannelsManager = new SelectedChannelsManager();
- _flashingManager = new FlashingBackgroundManager(null, ChannelsCanvas, null, this);
- _emergencyAlertPlayback = new WaveFilePlaybackManager(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "emergency.wav"));
-
- _channelHoldTimer = new System.Timers.Timer(10000);
- _channelHoldTimer.Elapsed += OnHoldTimerElapsed;
- _channelHoldTimer.AutoReset = true;
- _channelHoldTimer.Enabled = true;
-
- _waveIn = new WaveInEvent
- {
- WaveFormat = new WaveFormat(8000, 16, 1)
- };
- _waveIn.DataAvailable += WaveIn_DataAvailable;
- _waveIn.RecordingStopped += WaveIn_RecordingStopped;
-
- _waveIn.StartRecording();
-
- _audioManager = new AudioManager(_settingsManager);
-
- _selectedChannelsManager.SelectedChannelsChanged += SelectedChannelsChanged;
- Loaded += MainWindow_Loaded;
- }
-
- private void OpenCodeplug_Click(object sender, RoutedEventArgs e)
- {
- OpenFileDialog openFileDialog = new OpenFileDialog
- {
- Filter = "Codeplug Files (*.yml)|*.yml|All Files (*.*)|*.*",
- Title = "Open Codeplug"
- };
- if (openFileDialog.ShowDialog() == true)
- {
- LoadCodeplug(openFileDialog.FileName);
-
- _settingsManager.LastCodeplugPath = openFileDialog.FileName;
- _settingsManager.SaveSettings();
- }
- }
-
- private void ResetSettings_Click(object sender, RoutedEventArgs e)
- {
- if (File.Exists("UserSettings.json"))
- File.Delete("UserSettings.json");
- }
-
- private void LoadCodeplug(string filePath)
- {
- try
- {
- var deserializer = new DeserializerBuilder()
- .WithNamingConvention(CamelCaseNamingConvention.Instance)
- .IgnoreUnmatchedProperties()
- .Build();
-
- var yaml = File.ReadAllText(filePath);
- Codeplug = deserializer.Deserialize(yaml);
-
- GenerateChannelWidgets();
- }
- catch (Exception ex)
- {
- MessageBox.Show($"Error loading codeplug: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
-
- private void GenerateChannelWidgets()
- {
- ChannelsCanvas.Children.Clear();
- double offsetX = 20;
- double offsetY = 20;
-
- if (Codeplug != null)
- {
- foreach (var system in Codeplug.Systems)
- {
- var systemStatusBox = new SystemStatusBox(system.Name, system.Address, system.Port);
-
- if (_settingsManager.SystemStatusPositions.TryGetValue(system.Name, out var position))
- {
- Canvas.SetLeft(systemStatusBox, position.X);
- Canvas.SetTop(systemStatusBox, position.Y);
- }
- else
- {
- Canvas.SetLeft(systemStatusBox, offsetX);
- Canvas.SetTop(systemStatusBox, offsetY);
- }
-
- systemStatusBox.MouseLeftButtonDown += SystemStatusBox_MouseLeftButtonDown;
- systemStatusBox.MouseMove += SystemStatusBox_MouseMove;
- systemStatusBox.MouseRightButtonDown += SystemStatusBox_MouseRightButtonDown;
-
- ChannelsCanvas.Children.Add(systemStatusBox);
-
- offsetX += 225;
- if (offsetX + 220 > ChannelsCanvas.ActualWidth)
- {
- offsetX = 20;
- offsetY += 106;
- }
-
- if (File.Exists(system.AliasPath))
- system.RidAlias = AliasTools.LoadAliases(system.AliasPath);
-
- if (!system.IsDvm)
- {
- _webSocketManager.AddWebSocketHandler(system.Name);
-
- IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name);
- handler.OnVoiceChannelResponse += HandleVoiceResponse;
- handler.OnVoiceChannelRelease += HandleVoiceRelease;
- handler.OnEmergencyAlarmResponse += HandleEmergencyAlarmResponse;
- handler.OnAudioData += HandleReceivedAudio;
- handler.OnAffiliationUpdate += HandleAffiliationUpdate;
-
- handler.OnUnitRegistrationResponse += (response) =>
- {
- Dispatcher.Invoke(() =>
- {
- if (response.Status == (int)ResponseType.GRANT)
- {
- systemStatusBox.Background = (Brush)new BrushConverter().ConvertFrom("#FF00BC48");
- systemStatusBox.ConnectionState = "Connected";
- }
- else
- {
- systemStatusBox.Background = new SolidColorBrush(Colors.Red);
- systemStatusBox.ConnectionState = "Disconnected";
- }
- });
- };
-
- handler.OnClose += () =>
- {
- Dispatcher.Invoke(() =>
- {
- systemStatusBox.Background = new SolidColorBrush(Colors.Red);
- systemStatusBox.ConnectionState = "Disconnected";
- });
- };
-
- handler.OnOpen += () =>
- {
- Console.WriteLine("Peer connected");
- };
-
- handler.OnReconnecting += () =>
- {
- Console.WriteLine("Peer reconnecting");
- };
-
- Task.Run(() =>
- {
- handler.Connect(system.Address, system.Port, system.AuthKey);
-
- handler.OnGroupAffiliationResponse += (response) => { /* TODO */ };
-
- if (handler.IsConnected)
- {
- U_REG_REQ release = new U_REG_REQ
- {
- SrcId = system.Rid,
- Site = system.Site
- };
-
- handler.SendMessage(release.GetData());
- }
- else
- {
- systemStatusBox.Background = new SolidColorBrush(Colors.Red);
- systemStatusBox.ConnectionState = "Disconnected";
- }
- });
- } else
- {
- _fneSystemManager.AddFneSystem(system.Name, system, this);
-
- PeerSystem peer = _fneSystemManager.GetFneSystem(system.Name);
-
- peer.peer.PeerConnected += (sender, response) =>
- {
- Console.WriteLine("FNE Peer connected");
-
- Dispatcher.Invoke(() =>
- {
- systemStatusBox.Background = (Brush)new BrushConverter().ConvertFrom("#FF00BC48");
- systemStatusBox.ConnectionState = "Connected";
- });
- };
-
-
- peer.peer.PeerDisconnected += (response) =>
- {
- Console.WriteLine("FNE Peer disconnected");
-
- Dispatcher.Invoke(() =>
- {
- systemStatusBox.Background = new SolidColorBrush(Colors.Red);
- systemStatusBox.ConnectionState = "Disconnected";
- });
- };
-
- Task.Run(() =>
- {
- peer.Start();
- });
- }
-
- if (!_settingsManager.ShowSystemStatus)
- systemStatusBox.Visibility = Visibility.Collapsed;
-
- }
- }
-
- if (_settingsManager.ShowChannels && Codeplug != null)
- {
- foreach (var zone in Codeplug.Zones)
- {
- foreach (var channel in zone.Channels)
- {
- var channelBox = new ChannelBox(_selectedChannelsManager, _audioManager, channel.Name, channel.System, channel.Tgid);
-
- //channelBox.crypter.AddKey(channel.GetKeyId(), channel.GetAlgoId(), channel.GetEncryptionKey());
-
- systemStatuses.Add(channel.Name, new SlotStatus());
-
- if (_settingsManager.ChannelPositions.TryGetValue(channel.Name, out var position))
- {
- Canvas.SetLeft(channelBox, position.X);
- Canvas.SetTop(channelBox, position.Y);
- }
- else
- {
- Canvas.SetLeft(channelBox, offsetX);
- Canvas.SetTop(channelBox, offsetY);
- }
-
- channelBox.PTTButtonClicked += ChannelBox_PTTButtonClicked;
- channelBox.PageButtonClicked += ChannelBox_PageButtonClicked;
- channelBox.HoldChannelButtonClicked += ChannelBox_HoldChannelButtonClicked;
-
- channelBox.MouseLeftButtonDown += ChannelBox_MouseLeftButtonDown;
- channelBox.MouseMove += ChannelBox_MouseMove;
- channelBox.MouseRightButtonDown += ChannelBox_MouseRightButtonDown;
- ChannelsCanvas.Children.Add(channelBox);
-
- offsetX += 225;
-
- if (offsetX + 220 > ChannelsCanvas.ActualWidth)
- {
- offsetX = 20;
- offsetY += 106;
- }
- }
- }
- }
-
- if (_settingsManager.ShowAlertTones && Codeplug != null)
- {
- foreach (var alertPath in _settingsManager.AlertToneFilePaths)
- {
- var alertTone = new AlertTone(alertPath)
- {
- IsEditMode = isEditMode
- };
-
- alertTone.OnAlertTone += SendAlertTone;
-
- if (_settingsManager.AlertTonePositions.TryGetValue(alertPath, out var position))
- {
- Canvas.SetLeft(alertTone, position.X);
- Canvas.SetTop(alertTone, position.Y);
- }
- else
- {
- Canvas.SetLeft(alertTone, 20);
- Canvas.SetTop(alertTone, 20);
- }
-
- alertTone.MouseRightButtonUp += AlertTone_MouseRightButtonUp;
-
- ChannelsCanvas.Children.Add(alertTone);
- }
- }
-
- playbackChannelBox = new ChannelBox(_selectedChannelsManager, _audioManager, PLAYBACKCHNAME, PLAYBACKSYS, PLAYBACKTG);
-
- if (_settingsManager.ChannelPositions.TryGetValue(PLAYBACKCHNAME, out var pos))
- {
- Canvas.SetLeft(playbackChannelBox, pos.X);
- Canvas.SetTop(playbackChannelBox, pos.Y);
- }
- else
- {
- Canvas.SetLeft(playbackChannelBox, offsetX);
- Canvas.SetTop(playbackChannelBox, offsetY);
- }
-
- playbackChannelBox.PTTButtonClicked += ChannelBox_PTTButtonClicked;
- playbackChannelBox.PageButtonClicked += ChannelBox_PageButtonClicked;
- playbackChannelBox.HoldChannelButtonClicked += ChannelBox_HoldChannelButtonClicked;
-
- playbackChannelBox.MouseLeftButtonDown += ChannelBox_MouseLeftButtonDown;
- playbackChannelBox.MouseMove += ChannelBox_MouseMove;
- playbackChannelBox.MouseRightButtonDown += ChannelBox_MouseRightButtonDown;
- ChannelsCanvas.Children.Add(playbackChannelBox);
-
- //offsetX += 225;
-
- //if (offsetX + 220 > ChannelsCanvas.ActualWidth)
- //{
- // offsetX = 20;
- // offsetY += 106;
- //}
-
- AdjustCanvasHeight();
- }
-
- private void WaveIn_RecordingStopped(object sender, EventArgs e)
- {
- /* stub */
- }
-
- private void WaveIn_DataAvailable(object sender, WaveInEventArgs e)
- {
- bool isAnyTgOn = false;
-
- foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
- {
- if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
- {
- playbackChannelBox.IsReceiving = true;
- continue;
- }
-
- Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
- Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
-
- Task.Run(() =>
- {
-
- if (!system.IsDvm)
- {
- IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name);
-
- if (channel.IsSelected && channel.VoiceChannel != null && channel.PttState)
- {
- isAnyTgOn = true;
-
- object voicePaket = new
- {
- type = PacketType.AUDIO_DATA,
- data = new
- {
- Data = e.Buffer,
- VoiceChannel = new VoiceChannel
- {
- Frequency = channel.VoiceChannel,
- DstId = cpgChannel.Tgid,
- SrcId = system.Rid,
- Site = system.Site
- },
- Site = system.Site
- }
- };
-
- handler.SendMessage(voicePaket);
- }
- }
- else
- {
- PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
-
- if (channel.IsSelected && channel.PttState)
- {
- isAnyTgOn = true;
-
- int samples = 320;
-
- channel.chunkedPcm = AudioConverter.SplitToChunks(e.Buffer);
-
- foreach (byte[] chunk in channel.chunkedPcm)
- {
- if (chunk.Length == samples)
- {
- P25EncodeAudioFrame(chunk, handler, channel, cpgChannel, system);
- }
- else
- {
- Console.WriteLine("bad sample length: " + chunk.Length);
- }
- }
- }
- }
- });
- }
-
- if (isAnyTgOn && playbackChannelBox.IsSelected)
- _audioManager.AddTalkgroupStream(PLAYBACKTG, e.Buffer);
- }
-
- private void SelectedChannelsChanged()
- {
- foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
- {
- if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
- continue;
-
- Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
- Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
-
- if (!system.IsDvm) {
- IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name);
-
- if (channel.IsSelected && handler.IsConnected)
- {
- Console.WriteLine("sending WLINK master aff");
-
- Task.Run(() =>
- {
- GRP_AFF_REQ release = new GRP_AFF_REQ
- {
- SrcId = system.Rid,
- DstId = cpgChannel.Tgid,
- Site = system.Site
- };
-
- handler.SendMessage(release.GetData());
- });
- }
- } else
- {
- PeerSystem fne = _fneSystemManager.GetFneSystem(system.Name);
-
- if (channel.IsSelected)
- {
- uint newTgid = UInt32.Parse(cpgChannel.Tgid);
- bool exists = fneAffs.Any(aff => aff.Item2 == newTgid);
-
- if (cpgChannel.GetAlgoId() != 0 && cpgChannel.GetKeyId() != 0)
- fne.peer.SendMasterKeyRequest(cpgChannel.GetAlgoId(), cpgChannel.GetKeyId());
-
- if (!exists)
- fneAffs.Add(new Tuple(GetUniqueRid(system.Rid), newTgid));
-
- //Console.WriteLine("FNE Affiliations:");
- //foreach (var aff in fneAffs)
- //{
- // Console.WriteLine($" RID: {aff.Item1}, TGID: {aff.Item2}");
- //}
- }
- }
- }
-
- foreach (Codeplug.System system in Codeplug.Systems)
- {
- if (system.IsDvm)
- {
- PeerSystem fne = _fneSystemManager.GetFneSystem(system.Name);
- //fne.peer.SendMasterAffiliationUpdate(fneAffs);
- }
- }
- }
-
- private void AudioSettings_Click(object sender, RoutedEventArgs e)
- {
- List channels = Codeplug?.Zones.SelectMany(z => z.Channels).ToList() ?? new List();
-
- AudioSettingsWindow audioSettingsWindow = new AudioSettingsWindow(_settingsManager, _audioManager, channels);
- audioSettingsWindow.ShowDialog();
- }
-
- private void P25Page_Click(object sender, RoutedEventArgs e)
- {
- DigitalPageWindow pageWindow = new DigitalPageWindow(Codeplug.Systems);
- pageWindow.Owner = this;
- if (pageWindow.ShowDialog() == true)
- {
- if (!pageWindow.RadioSystem.IsDvm)
- {
- IPeer handler = _webSocketManager.GetWebSocketHandler(pageWindow.RadioSystem.Name);
-
- CALL_ALRT_REQ callAlert = new CALL_ALRT_REQ
- {
- SrcId = pageWindow.RadioSystem.Rid,
- DstId = pageWindow.DstId
- };
-
- handler.SendMessage(callAlert.GetData());
- }
- else
- {
- PeerSystem handler = _fneSystemManager.GetFneSystem(pageWindow.RadioSystem.Name);
- IOSP_CALL_ALRT callAlert = new IOSP_CALL_ALRT(UInt32.Parse(pageWindow.DstId), UInt32.Parse(pageWindow.RadioSystem.Rid));
-
- RemoteCallData callData = new RemoteCallData
- {
- SrcId = UInt32.Parse(pageWindow.RadioSystem.Rid),
- DstId = UInt32.Parse(pageWindow.DstId),
- LCO = P25Defines.TSBK_IOSP_CALL_ALRT
- };
-
- byte[] tsbk = new byte[P25Defines.P25_TSBK_LENGTH_BYTES];
- byte[] payload = new byte[P25Defines.P25_TSBK_LENGTH_BYTES];
-
- callAlert.Encode(ref tsbk, ref payload, true, true);
-
- handler.SendP25TSBK(callData, tsbk);
-
- Console.WriteLine("sent page");
- }
- }
- }
-
- private async void ManualPage_Click(object sender, RoutedEventArgs e)
- {
- QuickCallPage pageWindow = new QuickCallPage();
- pageWindow.Owner = this;
- if (pageWindow.ShowDialog() == true)
- {
- foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
- {
- Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
- Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
-
- if (channel.PageState)
- {
- ToneGenerator generator = new ToneGenerator();
-
- double toneADuration = 1.0;
- double toneBDuration = 3.0;
-
- byte[] toneA = generator.GenerateTone(Double.Parse(pageWindow.ToneA), toneADuration);
- byte[] toneB = generator.GenerateTone(Double.Parse(pageWindow.ToneB), toneBDuration);
-
- byte[] combinedAudio = new byte[toneA.Length + toneB.Length];
- Buffer.BlockCopy(toneA, 0, combinedAudio, 0, toneA.Length);
- Buffer.BlockCopy(toneB, 0, combinedAudio, toneA.Length, toneB.Length);
-
- int chunkSize = 1600;
-
- if (system.IsDvm)
- chunkSize = 320;
-
- int totalChunks = (combinedAudio.Length + chunkSize - 1) / chunkSize;
-
- Task.Run(() =>
- {
- //_waveProvider.ClearBuffer();
- _audioManager.AddTalkgroupStream(cpgChannel.Tgid, combinedAudio);
- });
-
- await Task.Run(() =>
- {
- for (int i = 0; i < totalChunks; i++)
- {
- int offset = i * chunkSize;
- int size = Math.Min(chunkSize, combinedAudio.Length - offset);
-
- byte[] chunk = new byte[chunkSize];
- Buffer.BlockCopy(combinedAudio, offset, chunk, 0, size);
-
- if (!system.IsDvm)
- {
- IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name);
-
- AudioPacket voicePacket = new AudioPacket
- {
- Data = chunk,
- VoiceChannel = new VoiceChannel
- {
- Frequency = channel.VoiceChannel,
- DstId = cpgChannel.Tgid,
- SrcId = system.Rid,
- Site = system.Site
- },
- Site = system.Site,
- LopServerVocode = true
- };
-
- handler.SendMessage(voicePacket.GetData());
- } else
- {
- PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
-
- if (chunk.Length == 320)
- {
- P25EncodeAudioFrame(chunk, handler, channel, cpgChannel, system);
- }
- }
- }
- });
-
- double totalDurationMs = (toneADuration + toneBDuration) * 1000 + 750;
- await Task.Delay((int)totalDurationMs);
-
- if (!system.IsDvm)
- {
- IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name);
-
- GRP_VCH_RLS release = new GRP_VCH_RLS
- {
- SrcId = system.Rid,
- DstId = cpgChannel.Tgid,
- Channel = channel.VoiceChannel,
- Site = system.Site
- };
-
- handler.SendMessage(release.GetData());
- } else
- {
- PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
-
- await Task.Delay(4000);
-
- handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), false);
- }
-
- Dispatcher.Invoke(() =>
- {
- //channel.PageState = false; // TODO: Investigate
- channel.PageSelectButton.Background = channel.grayGradient;
- });
- }
- }
- }
- }
-
- private void SendAlertTone(AlertTone e)
- {
- Task.Run(() => SendAlertTone(e.AlertFilePath));
- }
-
- private void SendAlertTone(string filePath, bool forHold = false)
- {
- if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
- {
- try
- {
- foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
- {
- if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
- continue;
-
- Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
- Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
-
- if (channel.PageState || (forHold && channel.HoldState))
- {
- byte[] pcmData;
-
- Task.Run(async () => {
- using (var waveReader = new WaveFileReader(filePath))
- {
- if (waveReader.WaveFormat.Encoding != WaveFormatEncoding.Pcm ||
- waveReader.WaveFormat.SampleRate != 8000 ||
- waveReader.WaveFormat.BitsPerSample != 16 ||
- waveReader.WaveFormat.Channels != 1)
- {
- MessageBox.Show("The alert tone must be PCM 16-bit, Mono, 8000Hz format.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
- return;
- }
-
- using (MemoryStream ms = new MemoryStream())
- {
- waveReader.CopyTo(ms);
- pcmData = ms.ToArray();
- }
- }
-
- int chunkSize = 1600;
- int totalChunks = (pcmData.Length + chunkSize - 1) / chunkSize;
-
- if (pcmData.Length % chunkSize != 0)
- {
- byte[] paddedData = new byte[totalChunks * chunkSize];
- Buffer.BlockCopy(pcmData, 0, paddedData, 0, pcmData.Length);
- pcmData = paddedData;
- }
-
- Task.Run(() =>
- {
- _audioManager.AddTalkgroupStream(cpgChannel.Tgid, pcmData);
- });
-
- DateTime startTime = DateTime.UtcNow;
-
- for (int i = 0; i < totalChunks; i++)
- {
- int offset = i * chunkSize;
- byte[] chunk = new byte[chunkSize];
- Buffer.BlockCopy(pcmData, offset, chunk, 0, chunkSize);
-
- if (!system.IsDvm)
- {
- IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name);
-
- AudioPacket voicePacket = new AudioPacket
- {
- Data = chunk,
- VoiceChannel = new VoiceChannel
- {
- Frequency = channel.VoiceChannel,
- DstId = cpgChannel.Tgid,
- SrcId = system.Rid,
- Site = system.Site
- },
- Site = system.Site,
- LopServerVocode = true
- };
-
- handler.SendMessage(voicePacket.GetData());
- }
- else
- {
- PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
-
- channel.chunkedPcm = AudioConverter.SplitToChunks(chunk);
-
- foreach (byte[] smallchunk in channel.chunkedPcm)
- {
- if (smallchunk.Length == 320)
- {
- P25EncodeAudioFrame(smallchunk, handler, channel, cpgChannel, system);
- }
- }
- }
-
- DateTime nextPacketTime = startTime.AddMilliseconds((i + 1) * 100);
- TimeSpan waitTime = nextPacketTime - DateTime.UtcNow;
-
- if (waitTime.TotalMilliseconds > 0)
- {
- await Task.Delay(waitTime);
- }
- }
-
- double totalDurationMs = ((double)pcmData.Length / 16000) + 250;
- await Task.Delay((int)totalDurationMs);
-
- if (!system.IsDvm)
- {
- IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name);
-
- GRP_VCH_RLS release = new GRP_VCH_RLS
- {
- SrcId = system.Rid,
- DstId = cpgChannel.Tgid,
- Channel = channel.VoiceChannel,
- Site = system.Site
- };
-
- Dispatcher.Invoke(() =>
- {
- handler.SendMessage(release.GetData());
-
- if (forHold)
- channel.PttButton.Background = channel.grayGradient;
- else
- channel.PageState = false;
- });
- } else
- {
- PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
-
- await Task.Delay(3000);
-
- handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), false);
-
- Dispatcher.Invoke(() =>
- {
- if (forHold)
- channel.PttButton.Background = channel.grayGradient;
- else
- channel.PageState = false;
- });
- }
- });
- }
- }
- }
- catch (Exception ex)
- {
- MessageBox.Show($"Failed to process alert tone: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
- else
- {
- MessageBox.Show("Alert file not set or file not found.", "Alert", MessageBoxButton.OK, MessageBoxImage.Warning);
- }
- }
-
- private void SelectWidgets_Click(object sender, RoutedEventArgs e)
- {
- WidgetSelectionWindow widgetSelectionWindow = new WidgetSelectionWindow();
- widgetSelectionWindow.Owner = this;
- if (widgetSelectionWindow.ShowDialog() == true)
- {
- _settingsManager.ShowSystemStatus = widgetSelectionWindow.ShowSystemStatus;
- _settingsManager.ShowChannels = widgetSelectionWindow.ShowChannels;
- _settingsManager.ShowAlertTones = widgetSelectionWindow.ShowAlertTones;
-
- GenerateChannelWidgets();
- _settingsManager.SaveSettings();
- }
- }
-
- private void HandleEmergencyAlarmResponse(EMRG_ALRM_RSP response)
- {
- bool forUs = false;
-
- foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
- {
- Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
- Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
-
- if (response.DstId == cpgChannel.Tgid)
- {
- forUs = true;
- channel.Emergency = true;
- channel.LastSrcId = response.SrcId;
- }
- }
-
- if (forUs)
- {
- Dispatcher.Invoke(() =>
- {
- _flashingManager.Start();
- _emergencyAlertPlayback.Start();
- });
- }
- }
-
- private void HandleReceivedAudio(AudioPacket audioPacket)
- {
- bool shouldReceive = false;
- string talkgroupId = audioPacket.VoiceChannel.DstId;
-
- foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
- {
- if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
- continue;
-
- Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
- Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
-
- if (system.IsDvm)
- continue;
-
- if (audioPacket.VoiceChannel.SrcId != system.Rid && audioPacket.VoiceChannel.Frequency == channel.VoiceChannel && audioPacket.VoiceChannel.DstId == cpgChannel.Tgid)
- shouldReceive = true;
- }
-
- if (shouldReceive)
- _audioManager.AddTalkgroupStream(talkgroupId, audioPacket.Data);
- }
-
- private void HandleAffiliationUpdate(AFF_UPDATE affUpdate)
- {
- foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
- {
- if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
- continue;
-
- Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
- Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
-
- if (system.IsDvm)
- continue;
-
- IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name);
-
- bool ridExists = affUpdate.Affiliations.Any(aff => aff.SrcId == system.Rid);
- bool tgidExists = affUpdate.Affiliations.Any(aff => aff.DstId == cpgChannel.Tgid);
-
- if (ridExists && tgidExists)
- {
- //Console.WriteLine("rid aff'ed");
- }
- else
- {
- //Console.WriteLine("rid not aff'ed");
- Task.Run(() =>
- {
- GRP_AFF_REQ affReq = new GRP_AFF_REQ
- {
- SrcId = system.Rid,
- DstId = cpgChannel.Tgid,
- Site = system.Site
- };
-
- handler.SendMessage(affReq.GetData());
- });
- }
- }
- }
-
- private void HandleVoiceRelease(GRP_VCH_RLS response)
- {
- foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
- {
- if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
- continue;
-
- Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
- Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
-
- if (system.IsDvm)
- continue;
-
- IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name);
-
- if (response.DstId == cpgChannel.Tgid && response.SrcId != system.Rid)
- {
- Dispatcher.Invoke(() =>
- {
- if (channel.IsSelected)
- {
- channel.Background = (Brush)new BrushConverter().ConvertFrom("#FF0B004B");
- channel.IsReceiving = false;
- }
- else
- {
- channel.Background = new SolidColorBrush(Colors.DarkGray);
- channel.IsReceiving = false;
- }
- });
-
- channel.VoiceChannel = null;
- }
- }
- }
-
- private void HandleVoiceResponse(GRP_VCH_RSP response)
- {
- foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
- {
- if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
- continue;
-
- Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
- Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
-
- if (system.IsDvm)
- continue;
-
- IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name);
-
- if (channel.PttState && response.Status == (int)ResponseType.GRANT && response.Channel != null && response.SrcId == system.Rid && response.DstId == cpgChannel.Tgid)
- {
- channel.VoiceChannel = response.Channel;
- }
- else if (response.Status == (int)ResponseType.GRANT && response.SrcId != system.Rid && response.DstId == cpgChannel.Tgid)
- {
- channel.VoiceChannel = response.Channel;
-
- string alias = string.Empty;
-
- try
- {
- alias = AliasTools.GetAliasByRid(system.RidAlias, int.Parse(response.SrcId));
- }
- catch (Exception) { }
-
- if (alias.IsNullOrEmpty())
- channel.LastSrcId = "Last SRC: " + response.SrcId;
- else
- channel.LastSrcId = "Last: " + alias;
-
- Dispatcher.Invoke(() =>
- {
- channel.Background = (Brush)new BrushConverter().ConvertFrom("#FF00BC48");
- channel.IsReceiving = true;
- });
- }
- else if ((channel.HoldState || channel.PageState) && response.Status == (int)ResponseType.GRANT && response.Channel != null && response.SrcId == system.Rid && response.DstId == cpgChannel.Tgid)
- {
- channel.VoiceChannel = response.Channel;
- }
- else
- {
- //Dispatcher.Invoke(() =>
- //{
- // if (channel.IsSelected)
- // channel.Background = new SolidColorBrush(Colors.DodgerBlue);
- // else
- // channel.Background = new SolidColorBrush(Colors.Gray);
- //});
-
- //channel.VoiceChannel = null;
- //_stopSending = true;
- }
- }
- }
-
- private void ChannelBox_HoldChannelButtonClicked(object sender, ChannelBox e)
- {
- if (e.SystemName == PLAYBACKSYS || e.ChannelName == PLAYBACKCHNAME || e.DstId == PLAYBACKTG)
- return;
-
- Codeplug.System system = Codeplug.GetSystemForChannel(e.ChannelName);
- Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(e.ChannelName);
-
- if (system.IsDvm)
- return;
-
- IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name);
- }
-
- private void ChannelBox_PageButtonClicked(object sender, ChannelBox e)
- {
- if (e.SystemName == PLAYBACKSYS || e.ChannelName == PLAYBACKCHNAME || e.DstId == PLAYBACKTG)
- return;
-
- Codeplug.System system = Codeplug.GetSystemForChannel(e.ChannelName);
- Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(e.ChannelName);
-
- if (!system.IsDvm)
- {
-
- IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name);
-
- if (e.PageState)
- {
- GRP_VCH_REQ request = new GRP_VCH_REQ
- {
- SrcId = system.Rid,
- DstId = cpgChannel.Tgid,
- Site = system.Site
- };
-
- handler.SendMessage(request.GetData());
- }
- else
- {
- GRP_VCH_RLS release = new GRP_VCH_RLS
- {
- SrcId = system.Rid,
- DstId = cpgChannel.Tgid,
- Channel = e.VoiceChannel,
- Site = system.Site
- };
-
- handler.SendMessage(release.GetData());
- e.VoiceChannel = null;
- }
- }
- else
- {
- PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
- if (e.PageState)
- {
- handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), true);
- }
- else
- {
- handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), false);
- }
- }
- }
-
- private void ChannelBox_PTTButtonClicked(object sender, ChannelBox e)
- {
- if (e.SystemName == PLAYBACKSYS || e.ChannelName == PLAYBACKCHNAME || e.DstId == PLAYBACKTG)
- return;
-
- Codeplug.System system = Codeplug.GetSystemForChannel(e.ChannelName);
- Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(e.ChannelName);
-
- if (!system.IsDvm)
- {
- IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name);
-
- if (!e.IsSelected)
- return;
-
- if (e.PttState)
- {
- GRP_VCH_REQ request = new GRP_VCH_REQ
- {
- SrcId = system.Rid,
- DstId = cpgChannel.Tgid,
- Site = system.Site
- };
-
- handler.SendMessage(request.GetData());
- }
- else
- {
- GRP_VCH_RLS release = new GRP_VCH_RLS
- {
- SrcId = system.Rid,
- DstId = cpgChannel.Tgid,
- Channel = e.VoiceChannel,
- Site = system.Site
- };
-
- handler.SendMessage(release.GetData());
- e.VoiceChannel = null;
- }
- } else
- {
- PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
-
- if (!e.IsSelected)
- return;
-
- FneUtils.Memset(e.mi, 0x00, P25Defines.P25_MI_LENGTH);
-
- uint srcId = UInt32.Parse(system.Rid);
- uint dstId = UInt32.Parse(cpgChannel.Tgid);
-
- if (e.PttState)
- {
- e.txStreamId = handler.NewStreamId();
-
- Console.WriteLine("sending grant demand " + dstId);
- handler.SendP25TDU(srcId, dstId, true);
- }
- else
- {
- Console.WriteLine("sending terminator " + dstId);
- handler.SendP25TDU(srcId, dstId, false);
- }
- }
- }
-
- private void ChannelBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
- {
- if (!isEditMode || !(sender is UIElement element)) return;
-
- _draggedElement = element;
- _startPoint = e.GetPosition(ChannelsCanvas);
- _offsetX = _startPoint.X - Canvas.GetLeft(_draggedElement);
- _offsetY = _startPoint.Y - Canvas.GetTop(_draggedElement);
- _isDragging = true;
-
- element.CaptureMouse();
- }
-
- private const int GridSize = 5;
-
- private void ChannelBox_MouseMove(object sender, MouseEventArgs e)
- {
- if (!isEditMode || !_isDragging || _draggedElement == null) return;
-
- Point currentPosition = e.GetPosition(ChannelsCanvas);
-
- // Calculate the new position with snapping to the grid
- double newLeft = Math.Round((currentPosition.X - _offsetX) / GridSize) * GridSize;
- double newTop = Math.Round((currentPosition.Y - _offsetY) / GridSize) * GridSize;
-
- // Ensure the box stays within canvas bounds
- newLeft = Math.Max(0, Math.Min(newLeft, ChannelsCanvas.ActualWidth - _draggedElement.RenderSize.Width));
- newTop = Math.Max(0, Math.Min(newTop, ChannelsCanvas.ActualHeight - _draggedElement.RenderSize.Height));
-
- // Apply snapped position
- Canvas.SetLeft(_draggedElement, newLeft);
- Canvas.SetTop(_draggedElement, newTop);
-
- // Save the new position if it's a ChannelBox
- if (_draggedElement is ChannelBox channelBox)
- {
- _settingsManager.UpdateChannelPosition(channelBox.ChannelName, newLeft, newTop);
- }
-
- AdjustCanvasHeight();
- }
-
- private void ChannelBox_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
- {
- if (!isEditMode || !_isDragging || _draggedElement == null) return;
-
- _isDragging = false;
- _draggedElement.ReleaseMouseCapture();
- _draggedElement = null;
- }
-
- private void SystemStatusBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) => ChannelBox_MouseLeftButtonDown(sender, e);
- private void SystemStatusBox_MouseMove(object sender, MouseEventArgs e) => ChannelBox_MouseMove(sender, e);
-
- private void SystemStatusBox_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
- {
- if (!isEditMode) return;
-
- if (sender is SystemStatusBox systemStatusBox)
- {
- double x = Canvas.GetLeft(systemStatusBox);
- double y = Canvas.GetTop(systemStatusBox);
- _settingsManager.SystemStatusPositions[systemStatusBox.SystemName] = new ChannelPosition { X = x, Y = y };
-
- ChannelBox_MouseRightButtonDown(sender, e);
-
- AdjustCanvasHeight();
- }
- }
-
- private void ToggleEditMode_Click(object sender, RoutedEventArgs e)
- {
- isEditMode = !isEditMode;
- var menuItem = (MenuItem)sender;
- menuItem.Header = isEditMode ? "Disable Edit Mode" : "Enable Edit Mode";
- UpdateEditModeForWidgets();
- }
-
- private void UpdateEditModeForWidgets()
- {
- foreach (var child in ChannelsCanvas.Children)
- {
- if (child is AlertTone alertTone)
- {
- alertTone.IsEditMode = isEditMode;
- }
-
- if (child is ChannelBox channelBox)
- {
- channelBox.IsEditMode = isEditMode;
- }
- }
- }
-
- private void AddAlertTone_Click(object sender, RoutedEventArgs e)
- {
- OpenFileDialog openFileDialog = new OpenFileDialog
- {
- Filter = "WAV Files (*.wav)|*.wav|All Files (*.*)|*.*",
- Title = "Select Alert Tone"
- };
-
- if (openFileDialog.ShowDialog() == true)
- {
- string alertFilePath = openFileDialog.FileName;
- var alertTone = new AlertTone(alertFilePath)
- {
- IsEditMode = isEditMode
- };
-
- alertTone.OnAlertTone += SendAlertTone;
-
- if (_settingsManager.AlertTonePositions.TryGetValue(alertFilePath, out var position))
- {
- Canvas.SetLeft(alertTone, position.X);
- Canvas.SetTop(alertTone, position.Y);
- }
- else
- {
- Canvas.SetLeft(alertTone, 20);
- Canvas.SetTop(alertTone, 20);
- }
-
- alertTone.MouseRightButtonUp += AlertTone_MouseRightButtonUp;
-
- ChannelsCanvas.Children.Add(alertTone);
- _settingsManager.UpdateAlertTonePaths(alertFilePath);
-
- AdjustCanvasHeight();
- }
- }
-
- private void AlertTone_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
- {
- if (!isEditMode) return;
-
- if (sender is AlertTone alertTone)
- {
- double x = Canvas.GetLeft(alertTone);
- double y = Canvas.GetTop(alertTone);
- _settingsManager.UpdateAlertTonePosition(alertTone.AlertFilePath, x, y);
-
- AdjustCanvasHeight();
- }
- }
-
- private void AdjustCanvasHeight()
- {
- double maxBottom = 0;
-
- foreach (UIElement child in ChannelsCanvas.Children)
- {
- double childBottom = Canvas.GetTop(child) + child.RenderSize.Height;
- if (childBottom > maxBottom)
- {
- maxBottom = childBottom;
- }
- }
-
- ChannelsCanvas.Height = maxBottom + 150;
- }
-
- private void MainWindow_Loaded(object sender, RoutedEventArgs e)
- {
- if (!string.IsNullOrEmpty(_settingsManager.LastCodeplugPath) && File.Exists(_settingsManager.LastCodeplugPath))
- {
- LoadCodeplug(_settingsManager.LastCodeplugPath);
- }
- else
- {
- GenerateChannelWidgets();
- }
- }
-
- private async void OnHoldTimerElapsed(object sender, ElapsedEventArgs e)
- {
- foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
- {
- if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
- continue;
-
- Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
- Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
-
- if (!system.IsDvm)
- {
-
- IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name);
-
- if (channel.HoldState && !channel.IsReceiving && !channel.PttState && !channel.PageState)
- {
- //Task.Factory.StartNew(async () =>
- //{
- Console.WriteLine("Sending channel hold beep");
-
- Dispatcher.Invoke(() => { channel.PttButton.Background = channel.redGradient; });
-
- GRP_VCH_REQ req = new GRP_VCH_REQ
- {
- SrcId = system.Rid,
- DstId = cpgChannel.Tgid,
- Site = system.Site
- };
-
- handler.SendMessage(req.GetData());
-
- await Task.Delay(1000);
-
- SendAlertTone("hold.wav", true);
- // });
- }
- } else
- {
- PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
-
- if (channel.HoldState && !channel.IsReceiving && !channel.PttState && !channel.PageState)
- {
- handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), true);
- await Task.Delay(1000);
-
- SendAlertTone("hold.wav", true);
- }
- }
- }
- }
-
- protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
- {
- _settingsManager.SaveSettings();
- base.OnClosing(e);
- Application.Current.Shutdown();
- }
-
- private void ClearEmergency_Click(object sender, RoutedEventArgs e)
- {
- _emergencyAlertPlayback.Stop();
- _flashingManager.Stop();
-
- foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
- {
- channel.Emergency = false;
- }
- }
-
- private void btnAlert1_Click(object sender, RoutedEventArgs e)
- {
- Dispatcher.Invoke(() => {
- SendAlertTone("alert1.wav");
- });
- }
-
- private void btnAlert2_Click(object sender, RoutedEventArgs e)
- {
- Dispatcher.Invoke(() =>
- {
- SendAlertTone("alert2.wav");
- });
- }
-
- private void btnAlert3_Click(object sender, RoutedEventArgs e)
- {
- Dispatcher.Invoke(() =>
- {
- SendAlertTone("alert3.wav");
- });
- }
-
- private async void btnGlobalPtt_Click(object sender, RoutedEventArgs e)
- {
- if (globalPttState)
- await Task.Delay(500);
-
- globalPttState = !globalPttState;
-
- foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
- {
- if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
- continue;
-
- Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
- Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
-
- if (!system.IsDvm)
- {
- IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name);
-
- if (!channel.IsSelected)
- continue;
-
- if (globalPttState)
- {
- Dispatcher.Invoke(() =>
- {
- btnGlobalPtt.Background = channel.redGradient;
- });
-
-
- GRP_VCH_REQ request = new GRP_VCH_REQ
- {
- SrcId = system.Rid,
- DstId = cpgChannel.Tgid,
- Site = system.Site
- };
-
- channel.PttState = true;
-
- handler.SendMessage(request.GetData());
- }
- else
- {
- Dispatcher.Invoke(() =>
- {
- btnGlobalPtt.Background = channel.grayGradient;
- });
-
- GRP_VCH_RLS release = new GRP_VCH_RLS
- {
- SrcId = system.Rid,
- DstId = cpgChannel.Tgid,
- Site = system.Site
- };
-
- channel.PttState = false;
-
- handler.SendMessage(release.GetData());
- }
- } else
- {
- PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
-
- channel.txStreamId = handler.NewStreamId();
-
- if (globalPttState)
- {
- Dispatcher.Invoke(() =>
- {
- btnGlobalPtt.Background = channel.redGradient;
- channel.PttState = true;
- });
-
- handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), true);
- }
- else
- {
- Dispatcher.Invoke(() =>
- {
- btnGlobalPtt.Background = channel.grayGradient;
- channel.PttState = false;
- });
-
- handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), false);
- }
- }
- }
- }
-
- private void Button_Click(object sender, RoutedEventArgs e) { /* sub */ }
-
- private void SelectAll_Click(object sender, RoutedEventArgs e)
- {
- foreach (ChannelBox channel in ChannelsCanvas.Children.OfType())
- {
- if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
- continue;
-
- Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
- Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
-
- if (!channel.IsSelected)
- {
- channel.IsSelected = true;
-
- channel.Background = channel.IsSelected ? (Brush)new BrushConverter().ConvertFrom("#FF0B004B") : Brushes.Gray;
-
- if (channel.IsSelected)
- {
- _selectedChannelsManager.AddSelectedChannel(channel);
- }
- else
- {
- _selectedChannelsManager.RemoveSelectedChannel(channel);
- }
- }
- }
- }
-
- ///
- /// Helper to encode and transmit PCM audio as P25 IMBE frames.
- ///
- private void P25EncodeAudioFrame(byte[] pcm, PeerSystem handler, ChannelBox channel, Codeplug.Channel cpgChannel, Codeplug.System system)
- {
- bool encryptCall = true; // TODO: make this dynamic somewhere?
-
- if (channel.p25N > 17)
- channel.p25N = 0;
- if (channel.p25N == 0)
- FneUtils.Memset(channel.netLDU1, 0, 9 * 25);
- if (channel.p25N == 9)
- FneUtils.Memset(channel.netLDU2, 0, 9 * 25);
-
- // Log.Logger.Debug($"BYTE BUFFER {FneUtils.HexDump(pcm)}");
-
- //// pre-process: apply gain to PCM audio frames
- //if (Program.Configuration.TxAudioGain != 1.0f)
- //{
- // BufferedWaveProvider buffer = new BufferedWaveProvider(waveFormat);
- // buffer.AddSamples(pcm, 0, pcm.Length);
-
- // VolumeWaveProvider16 gainControl = new VolumeWaveProvider16(buffer);
- // gainControl.Volume = Program.Configuration.TxAudioGain;
- // gainControl.Read(pcm, 0, pcm.Length);
- //}
-
- int smpIdx = 0;
- short[] samples = new short[FneSystemBase.MBE_SAMPLES_LENGTH];
- for (int pcmIdx = 0; pcmIdx < pcm.Length; pcmIdx += 2)
- {
- samples[smpIdx] = (short)((pcm[pcmIdx + 1] << 8) + pcm[pcmIdx + 0]);
- smpIdx++;
- }
-
- // Convert to floats
- float[] fSamples = AudioConverter.PcmToFloat(samples);
-
- // Convert to signal
- DiscreteSignal signal = new DiscreteSignal(8000, fSamples, true);
-
- // Log.Logger.Debug($"SAMPLE BUFFER {FneUtils.HexDump(samples)}");
-
- // encode PCM samples into IMBE codewords
- byte[] imbe = new byte[FneSystemBase.IMBE_BUF_LEN];
-
-
- int tone = 0;
-
- if (true) // TODO: Disable/enable detection
- {
- tone = channel.toneDetector.Detect(signal);
- }
- if (tone > 0)
- {
- MBEToneGenerator.IMBEEncodeSingleTone((ushort)tone, imbe);
- Console.WriteLine($"({system.Name}) P25D: {tone} HZ TONE DETECT");
- }
- else
- {
-#if WIN32
- if (channel.extFullRateVocoder == null)
- channel.extFullRateVocoder = new AmbeVocoder(true);
-
- channel.extFullRateVocoder.encode(samples, out imbe);
-#else
- if (channel.encoder == null)
- channel.encoder = new MBEEncoder(MBE_MODE.IMBE_88BIT);
-
- channel.encoder.encode(samples, imbe);
-#endif
- }
- // Log.Logger.Debug($"IMBE {FneUtils.HexDump(imbe)}");
-
- if (encryptCall && cpgChannel.GetAlgoId() != 0 && cpgChannel.GetKeyId() != 0)
- {
- // initial HDU MI
- if (channel.p25N == 0)
- {
- if (channel.mi.All(b => b == 0))
- {
- Random random = new Random();
-
- for (int i = 0; i < P25Defines.P25_MI_LENGTH; i++)
- {
- channel.mi[i] = (byte)random.Next(0x00, 0x100);
- }
- }
-
- channel.crypter.Prepare(cpgChannel.GetAlgoId(), cpgChannel.GetKeyId(), ProtocolType.P25Phase1, channel.mi);
- }
-
- // crypto time
- channel.crypter.Process(imbe, channel.p25N < 9U ? P25Crypto.FrameType.LDU1 : P25Crypto.FrameType.LDU2, 0);
-
- // last block of LDU2, prepare a new MI
- if (channel.p25N == 17U)
- {
- P25Crypto.CycleP25Lfsr(channel.mi);
- channel.crypter.Prepare(cpgChannel.GetAlgoId(), cpgChannel.GetKeyId(), ProtocolType.P25Phase1, channel.mi);
- }
- }
-
- // fill the LDU buffers appropriately
- switch (channel.p25N)
- {
- // LDU1
- case 0:
- Buffer.BlockCopy(imbe, 0, channel.netLDU1, 10, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 1:
- Buffer.BlockCopy(imbe, 0, channel.netLDU1, 26, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 2:
- Buffer.BlockCopy(imbe, 0, channel.netLDU1, 55, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 3:
- Buffer.BlockCopy(imbe, 0, channel.netLDU1, 80, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 4:
- Buffer.BlockCopy(imbe, 0, channel.netLDU1, 105, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 5:
- Buffer.BlockCopy(imbe, 0, channel.netLDU1, 130, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 6:
- Buffer.BlockCopy(imbe, 0, channel.netLDU1, 155, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 7:
- Buffer.BlockCopy(imbe, 0, channel.netLDU1, 180, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 8:
- Buffer.BlockCopy(imbe, 0, channel.netLDU1, 204, FneSystemBase.IMBE_BUF_LEN);
- break;
-
- // LDU2
- case 9:
- Buffer.BlockCopy(imbe, 0, channel.netLDU2, 10, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 10:
- Buffer.BlockCopy(imbe, 0, channel.netLDU2, 26, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 11:
- Buffer.BlockCopy(imbe, 0, channel.netLDU2, 55, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 12:
- Buffer.BlockCopy(imbe, 0, channel.netLDU2, 80, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 13:
- Buffer.BlockCopy(imbe, 0, channel.netLDU2, 105, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 14:
- Buffer.BlockCopy(imbe, 0, channel.netLDU2, 130, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 15:
- Buffer.BlockCopy(imbe, 0, channel.netLDU2, 155, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 16:
- Buffer.BlockCopy(imbe, 0, channel.netLDU2, 180, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 17:
- Buffer.BlockCopy(imbe, 0, channel.netLDU2, 204, FneSystemBase.IMBE_BUF_LEN);
- break;
- }
-
- uint srcId = UInt32.Parse(system.Rid);
- uint dstId = UInt32.Parse(cpgChannel.Tgid);
-
- FnePeer peer = handler.peer;
- RemoteCallData callData = new RemoteCallData()
- {
- SrcId = srcId,
- DstId = dstId,
- LCO = P25Defines.LC_GROUP
- };
-
- // send P25 LDU1
- if (channel.p25N == 8U)
- {
- ushort pktSeq = 0;
- if (channel.p25SeqNo == 0U)
- pktSeq = peer.pktSeq(true);
- else
- pktSeq = peer.pktSeq();
-
- //Console.WriteLine($"({channel.SystemName}) P25D: Traffic *VOICE FRAME * PEER {handler.PeerId} SRC_ID {srcId} TGID {dstId} [STREAM ID {channel.txStreamId}]");
-
- byte[] payload = new byte[200];
- handler.CreateNewP25MessageHdr((byte)P25DUID.LDU1, callData, ref payload, cpgChannel.GetAlgoId(), cpgChannel.GetKeyId(), channel.mi);
- handler.CreateP25LDU1Message(channel.netLDU1, ref payload, srcId, dstId);
-
- peer.SendMaster(new Tuple(Constants.NET_FUNC_PROTOCOL, Constants.NET_PROTOCOL_SUBFUNC_P25), payload, pktSeq, channel.txStreamId);
- }
-
- // send P25 LDU2
- if (channel.p25N == 17U)
- {
- ushort pktSeq = 0;
- if (channel.p25SeqNo == 0U)
- pktSeq = peer.pktSeq(true);
- else
- pktSeq = peer.pktSeq();
-
- //Console.WriteLine($"({channel.SystemName}) P25D: Traffic *VOICE FRAME * PEER {handler.PeerId} SRC_ID {srcId} TGID {dstId} [STREAM ID {channel.txStreamId}]");
-
- byte[] payload = new byte[200];
- handler.CreateNewP25MessageHdr((byte)P25DUID.LDU2, callData, ref payload, cpgChannel.GetAlgoId(), cpgChannel.GetKeyId(), channel.mi);
- handler.CreateP25LDU2Message(channel.netLDU2, ref payload, new CryptoParams { AlgId = cpgChannel.GetAlgoId(), KeyId = cpgChannel.GetKeyId(), Mi = channel.mi });
-
- peer.SendMaster(new Tuple(Constants.NET_FUNC_PROTOCOL, Constants.NET_PROTOCOL_SUBFUNC_P25), payload, pktSeq, channel.txStreamId);
- }
-
- channel.p25SeqNo++;
- channel.p25N++;
- }
-
- ///
- /// Helper to decode and playback P25 IMBE frames as PCM audio.
- ///
- ///
- ///
- private void P25DecodeAudioFrame(byte[] ldu, P25DataReceivedEvent e, PeerSystem system, ChannelBox channel, bool emergency = false, P25Crypto.FrameType frameType = P25Crypto.FrameType.LDU1)
- {
- try
- {
- // decode 9 IMBE codewords into PCM samples
- for (int n = 0; n < 9; n++)
- {
- byte[] imbe = new byte[FneSystemBase.IMBE_BUF_LEN];
- switch (n)
- {
- case 0:
- Buffer.BlockCopy(ldu, 10, imbe, 0, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 1:
- Buffer.BlockCopy(ldu, 26, imbe, 0, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 2:
- Buffer.BlockCopy(ldu, 55, imbe, 0, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 3:
- Buffer.BlockCopy(ldu, 80, imbe, 0, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 4:
- Buffer.BlockCopy(ldu, 105, imbe, 0, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 5:
- Buffer.BlockCopy(ldu, 130, imbe, 0, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 6:
- Buffer.BlockCopy(ldu, 155, imbe, 0, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 7:
- Buffer.BlockCopy(ldu, 180, imbe, 0, FneSystemBase.IMBE_BUF_LEN);
- break;
- case 8:
- Buffer.BlockCopy(ldu, 204, imbe, 0, FneSystemBase.IMBE_BUF_LEN);
- break;
- }
-
- //Log.Logger.Debug($"Decoding IMBE buffer: {FneUtils.HexDump(imbe)}");
-
- short[] samples = new short[FneSystemBase.MBE_SAMPLES_LENGTH];
-
- channel.crypter.Process(imbe, frameType, n);
-
-#if WIN32
- if (channel.extFullRateVocoder == null)
- channel.extFullRateVocoder = new AmbeVocoder(true);
-
- channel.p25Errs = channel.extFullRateVocoder.decode(imbe, out samples);
-#else
-
- channel.p25Errs = channel.decoder.decode(imbe, samples);
-#endif
-
- if (emergency)
- {
- if (!channel.Emergency)
- {
- Task.Run(() =>
- {
- HandleEmergencyAlarmResponse(new EMRG_ALRM_RSP
- {
- SrcId = e.SrcId.ToString(),
- DstId = e.DstId.ToString()
- });
- });
- }
- }
-
- if (samples != null)
- {
- //Log.Logger.Debug($"({Config.Name}) P25D: Traffic *VOICE FRAME * PEER {e.PeerId} SRC_ID {e.SrcId} TGID {e.DstId} VC{n} ERRS {errs} [STREAM ID {e.StreamId}]");
- //Log.Logger.Debug($"IMBE {FneUtils.HexDump(imbe)}");
- //Console.WriteLine($"SAMPLE BUFFER {FneUtils.HexDump(samples)}");
-
- int pcmIdx = 0;
- byte[] pcmData = new byte[samples.Length * 2];
- for (int i = 0; i < samples.Length; i++)
- {
- pcmData[pcmIdx] = (byte)(samples[i] & 0xFF);
- pcmData[pcmIdx + 1] = (byte)((samples[i] >> 8) & 0xFF);
- pcmIdx += 2;
- }
-
- _audioManager.AddTalkgroupStream(e.DstId.ToString(), pcmData);
- }
- }
- }
- catch (Exception ex)
- {
- Console.WriteLine($"Audio Decode Exception: {ex.Message}");
- }
- }
-
- private uint GetUniqueRid(string ridString)
- {
- uint rid;
-
- // Try to parse the RID, default to 1000 if parsing fails
- if (!UInt32.TryParse(ridString, out rid))
- {
- rid = 1000;
- }
-
- // Ensure uniqueness by incrementing if needed
- while (usedRids.Contains(rid))
- {
- rid++;
- }
-
- // Store the new unique RID
- usedRids.Add(rid);
-
- return rid;
- }
-
- ///
- ///
- ///
- ///
- public void KeyResponseReceived(KeyResponseEvent e)
- {
- //Console.WriteLine($"Message ID: {e.KmmKey.MessageId}");
- //Console.WriteLine($"Decrypt Info Format: {e.KmmKey.DecryptInfoFmt}");
- //Console.WriteLine($"Algorithm ID: {e.KmmKey.AlgId}");
- //Console.WriteLine($"Key ID: {e.KmmKey.KeyId}");
- //Console.WriteLine($"Keyset ID: {e.KmmKey.KeysetItem.KeysetId}");
- //Console.WriteLine($"Keyset Alg ID: {e.KmmKey.KeysetItem.AlgId}");
- //Console.WriteLine($"Keyset Key Length: {e.KmmKey.KeysetItem.KeyLength}");
- //Console.WriteLine($"Number of Keys: {e.KmmKey.KeysetItem.Keys.Count}");
-
- foreach (var key in e.KmmKey.KeysetItem.Keys)
- {
- //Console.WriteLine($" Key Format: {key.KeyFormat}");
- //Console.WriteLine($" SLN: {key.Sln}");
- //Console.WriteLine($" Key ID: {key.KeyId}");
- //Console.WriteLine($" Key Data: {BitConverter.ToString(key.GetKey())}");
-
- Dispatcher.Invoke(() =>
- {
- foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
- {
- Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
- Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
-
- if (!system.IsDvm)
- continue;
-
- PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
-
- if (cpgChannel.GetKeyId() != 0 && cpgChannel.GetAlgoId() != 0)
- channel.crypter.AddKey(key.KeyId, e.KmmKey.KeysetItem.AlgId, key.GetKey());
- }
- });
- }
- }
-
- private void KeyStatus_Click(object sender, RoutedEventArgs e)
- {
- KeyStatusWindow keyStatus = new KeyStatusWindow(Codeplug, this);
- keyStatus.Show();
- }
-
- ///
- /// Event handler used to process incoming P25 data.
- ///
- ///
- ///
- public void P25DataReceived(P25DataReceivedEvent e, DateTime pktTime)
- {
- uint sysId = (uint)((e.Data[11U] << 8) | (e.Data[12U] << 0));
- uint netId = FneUtils.Bytes3ToUInt32(e.Data, 16);
- byte control = e.Data[14U];
-
- byte len = e.Data[23];
- byte[] data = new byte[len];
- for (int i = 24; i < len; i++)
- data[i - 24] = e.Data[i];
-
- Dispatcher.Invoke(() =>
- {
- foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
- {
- Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
- Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
-
- bool isEmergency = false;
- bool encrypted = false;
-
- if (!system.IsDvm)
- continue;
-
- PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
-
- if (!channel.IsEnabled)
- continue;
-
- if (cpgChannel.Tgid != e.DstId.ToString())
- continue;
-
- if (!systemStatuses.ContainsKey(cpgChannel.Name))
- {
- systemStatuses[cpgChannel.Name] = new SlotStatus();
- }
-
- if (channel.decoder == null)
- {
- channel.decoder = new MBEDecoder(MBE_MODE.IMBE_88BIT);
- }
-
- SlotStatus slot = systemStatuses[cpgChannel.Name];
-
- // if this is an LDU1 see if this is the first LDU with HDU encryption data
- if (e.DUID == P25DUID.LDU1)
- {
- byte frameType = e.Data[180];
-
- // get the initial MI and other enc info (bug found by the screeeeeeeeech on initial tx...)
- if (frameType == P25Defines.P25_FT_HDU_VALID)
- {
- channel.algId = e.Data[181];
- channel.kId = (ushort)((e.Data[182] << 8) | e.Data[183]);
- Array.Copy(e.Data, 184, channel.mi, 0, P25Defines.P25_MI_LENGTH);
-
- channel.crypter.Prepare(channel.algId, channel.kId, P25Crypto.ProtocolType.P25Phase1, channel.mi);
-
- encrypted = true;
- }
- }
-
- // is this a new call stream?
- if (e.StreamId != slot.RxStreamId && ((e.DUID != P25DUID.TDU) && (e.DUID != P25DUID.TDULC)))
- {
- channel.IsReceiving = true;
- slot.RxStart = pktTime;
- Console.WriteLine($"({system.Name}) P25D: Traffic *CALL START * PEER {e.PeerId} SRC_ID {e.SrcId} TGID {e.DstId} [STREAM ID {e.StreamId}]");
-
- FneUtils.Memset(channel.mi, 0x00, P25Defines.P25_MI_LENGTH);
-
- callHistoryWindow.AddCall(cpgChannel.Name, (int)e.SrcId, (int)e.DstId);
- callHistoryWindow.ChannelKeyed(cpgChannel.Name, (int)e.SrcId, encrypted);
-
- string alias = string.Empty;
-
- try
- {
- alias = AliasTools.GetAliasByRid(system.RidAlias, (int)e.SrcId);
- }
- catch (Exception) { }
-
- if (alias.IsNullOrEmpty())
- channel.LastSrcId = "Last SRC: " + e.SrcId;
- else
- channel.LastSrcId = "Last: " + alias;
-
- if (channel.algId != P25Defines.P25_ALGO_UNENCRYPT)
- channel.Background = (Brush)new BrushConverter().ConvertFrom("#ffdeaf0a");
- else
- channel.Background = (Brush)new BrushConverter().ConvertFrom("#FF00BC48");
- }
-
- // Is the call over?
- if (((e.DUID == P25DUID.TDU) || (e.DUID == P25DUID.TDULC)) && (slot.RxType != fnecore.FrameType.TERMINATOR))
- {
- channel.IsReceiving = false;
- TimeSpan callDuration = pktTime - slot.RxStart;
- Console.WriteLine($"({system.Name}) P25D: Traffic *CALL END * PEER {e.PeerId} SRC_ID {e.SrcId} TGID {e.DstId} DUR {callDuration} [STREAM ID {e.StreamId}]");
- channel.Background = (Brush)new BrushConverter().ConvertFrom("#FF0B004B");
- callHistoryWindow.ChannelUnkeyed(cpgChannel.Name, (int)e.SrcId);
- return;
- }
-
- if ((channel.algId != cpgChannel.GetAlgoId() || channel.kId != cpgChannel.GetKeyId()) && channel.algId != P25Defines.P25_ALGO_UNENCRYPT)
- continue;
-
- byte[] newMI = new byte[P25Defines.P25_MI_LENGTH];
-
- int count = 0;
-
- switch (e.DUID)
- {
- case P25DUID.LDU1:
- {
- // The '62', '63', '64', '65', '66', '67', '68', '69', '6A' records are LDU1
- if ((data[0U] == 0x62U) && (data[22U] == 0x63U) &&
- (data[36U] == 0x64U) && (data[53U] == 0x65U) &&
- (data[70U] == 0x66U) && (data[87U] == 0x67U) &&
- (data[104U] == 0x68U) && (data[121U] == 0x69U) &&
- (data[138U] == 0x6AU))
- {
- // The '62' record - IMBE Voice 1
- Buffer.BlockCopy(data, count, channel.netLDU1, 0, 22);
- count += 22;
-
- // The '63' record - IMBE Voice 2
- Buffer.BlockCopy(data, count, channel.netLDU1, 25, 14);
- count += 14;
-
- // The '64' record - IMBE Voice 3 + Link Control
- Buffer.BlockCopy(data, count, channel.netLDU1, 50, 17);
- byte serviceOptions = data[count + 3];
- isEmergency = (serviceOptions & 0x80) == 0x80;
- count += 17;
-
- // The '65' record - IMBE Voice 4 + Link Control
- Buffer.BlockCopy(data, count, channel.netLDU1, 75, 17);
- count += 17;
-
- // The '66' record - IMBE Voice 5 + Link Control
- Buffer.BlockCopy(data, count, channel.netLDU1, 100, 17);
- count += 17;
-
- // The '67' record - IMBE Voice 6 + Link Control
- Buffer.BlockCopy(data, count, channel.netLDU1, 125, 17);
- count += 17;
-
- // The '68' record - IMBE Voice 7 + Link Control
- Buffer.BlockCopy(data, count, channel.netLDU1, 150, 17);
- count += 17;
-
- // The '69' record - IMBE Voice 8 + Link Control
- Buffer.BlockCopy(data, count, channel.netLDU1, 175, 17);
- count += 17;
-
- // The '6A' record - IMBE Voice 9 + Low Speed Data
- Buffer.BlockCopy(data, count, channel.netLDU1, 200, 16);
- count += 16;
-
- // decode 9 IMBE codewords into PCM samples
- P25DecodeAudioFrame(channel.netLDU1, e, handler, channel, isEmergency);
- }
- }
- break;
- case P25DUID.LDU2:
- {
- // The '6B', '6C', '6D', '6E', '6F', '70', '71', '72', '73' records are LDU2
- if ((data[0U] == 0x6BU) && (data[22U] == 0x6CU) &&
- (data[36U] == 0x6DU) && (data[53U] == 0x6EU) &&
- (data[70U] == 0x6FU) && (data[87U] == 0x70U) &&
- (data[104U] == 0x71U) && (data[121U] == 0x72U) &&
- (data[138U] == 0x73U))
- {
- // The '6B' record - IMBE Voice 10
- Buffer.BlockCopy(data, count, channel.netLDU2, 0, 22);
- count += 22;
-
- // The '6C' record - IMBE Voice 11
- Buffer.BlockCopy(data, count, channel.netLDU2, 25, 14);
- count += 14;
-
- // The '6D' record - IMBE Voice 12 + Encryption Sync
- Buffer.BlockCopy(data, count, channel.netLDU2, 50, 17);
- newMI[0] = data[count + 1];
- newMI[1] = data[count + 2];
- newMI[2] = data[count + 3];
- count += 17;
-
- // The '6E' record - IMBE Voice 13 + Encryption Sync
- Buffer.BlockCopy(data, count, channel.netLDU2, 75, 17);
- newMI[3] = data[count + 1];
- newMI[4] = data[count + 2];
- newMI[5] = data[count + 3];
- count += 17;
-
- // The '6F' record - IMBE Voice 14 + Encryption Sync
- Buffer.BlockCopy(data, count, channel.netLDU2, 100, 17);
- newMI[6] = data[count + 1];
- newMI[7] = data[count + 2];
- newMI[8] = data[count + 3];
- count += 17;
-
- // The '70' record - IMBE Voice 15 + Encryption Sync
- Buffer.BlockCopy(data, count, channel.netLDU2, 125, 17);
- channel.algId = data[count + 1]; // Algorithm ID
- channel.kId = (ushort)((data[count + 2] << 8) | data[count + 3]); // Key ID
- count += 17;
-
- // The '71' record - IMBE Voice 16 + Encryption Sync
- Buffer.BlockCopy(data, count, channel.netLDU2, 150, 17);
- count += 17;
-
- // The '72' record - IMBE Voice 17 + Encryption Sync
- Buffer.BlockCopy(data, count, channel.netLDU2, 175, 17);
- count += 17;
-
- // The '73' record - IMBE Voice 18 + Low Speed Data
- Buffer.BlockCopy(data, count, channel.netLDU2, 200, 16);
- count += 16;
-
- if (channel.p25Errs > 0) // temp, need to actually get errors I guess
- P25Crypto.CycleP25Lfsr(channel.mi);
- else
- Array.Copy(newMI, channel.mi, P25Defines.P25_MI_LENGTH);
-
- // decode 9 IMBE codewords into PCM samples
- P25DecodeAudioFrame(channel.netLDU2, e, handler, channel, isEmergency, P25Crypto.FrameType.LDU2);
- }
- }
- break;
- }
-
- if (channel.mi != null)
- channel.crypter.Prepare(channel.algId, channel.kId, P25Crypto.ProtocolType.P25Phase1, channel.mi);
-
- slot.RxRFS = e.SrcId;
- slot.RxType = e.FrameType;
- slot.RxTGId = e.DstId;
- slot.RxTime = pktTime;
- slot.RxStreamId = e.StreamId;
-
- }
- });
- }
-
- private void CallHist_Click(object sender, RoutedEventArgs e)
- {
- callHistoryWindow.Show();
- }
- }
-}
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - DVMConsole
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / DVM Console
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
+*
+* Copyright (C) 2024-2025 Caleb, K4PHP
+* Copyright (C) 2025 J. Dean
+*
+*/
+
+using Microsoft.Win32;
+using System;
+using System.Timers;
+using System.IO;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using YamlDotNet.Serialization;
+using YamlDotNet.Serialization.NamingConventions;
+using DVMConsole.Controls;
+using System.Windows.Media;
+using NAudio.Wave;
+using fnecore.P25;
+using fnecore;
+using Constants = fnecore.Constants;
+using fnecore.P25.LC.TSBK;
+using NWaves.Signals;
+using static DVMConsole.P25Crypto;
+
+namespace DVMConsole
+{
+ public partial class MainWindow : Window
+ {
+ public Codeplug Codeplug { get; set; }
+ private bool isEditMode = false;
+
+ private bool globalPttState = false;
+
+ private const int GridSize = 5;
+
+ private UIElement _draggedElement;
+ private Point _startPoint;
+ private double _offsetX;
+ private double _offsetY;
+ private bool _isDragging;
+
+ private SettingsManager _settingsManager = new SettingsManager();
+ private SelectedChannelsManager _selectedChannelsManager;
+ private FlashingBackgroundManager _flashingManager;
+ private WaveFilePlaybackManager _emergencyAlertPlayback;
+
+ private ChannelBox playbackChannelBox;
+
+ CallHistoryWindow callHistoryWindow = new CallHistoryWindow();
+
+ public static string PLAYBACKTG = "LOCPLAYBACK";
+ public static string PLAYBACKSYS = "LOCPLAYBACKSYS";
+ public static string PLAYBACKCHNAME = "PLAYBACK";
+
+ private readonly WaveInEvent _waveIn;
+ private readonly AudioManager _audioManager;
+
+ private static System.Timers.Timer _channelHoldTimer;
+
+ private Dictionary systemStatuses = new Dictionary();
+ private FneSystemManager _fneSystemManager = new FneSystemManager();
+
+ public MainWindow()
+ {
+#if !DEBUG
+ ConsoleNative.ShowConsole();
+#endif
+ InitializeComponent();
+ _settingsManager.LoadSettings();
+ _selectedChannelsManager = new SelectedChannelsManager();
+ _flashingManager = new FlashingBackgroundManager(null, ChannelsCanvas, null, this);
+ _emergencyAlertPlayback = new WaveFilePlaybackManager(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "emergency.wav"));
+
+ _channelHoldTimer = new System.Timers.Timer(10000);
+ _channelHoldTimer.Elapsed += OnHoldTimerElapsed;
+ _channelHoldTimer.AutoReset = true;
+ _channelHoldTimer.Enabled = true;
+
+ _waveIn = new WaveInEvent
+ {
+ WaveFormat = new WaveFormat(8000, 16, 1)
+ };
+ _waveIn.DataAvailable += WaveIn_DataAvailable;
+ _waveIn.RecordingStopped += WaveIn_RecordingStopped;
+
+ _waveIn.StartRecording();
+
+ _audioManager = new AudioManager(_settingsManager);
+
+ _selectedChannelsManager.SelectedChannelsChanged += SelectedChannelsChanged;
+ Loaded += MainWindow_Loaded;
+ }
+
+ private void OpenCodeplug_Click(object sender, RoutedEventArgs e)
+ {
+ OpenFileDialog openFileDialog = new OpenFileDialog
+ {
+ Filter = "Codeplug Files (*.yml)|*.yml|All Files (*.*)|*.*",
+ Title = "Open Codeplug"
+ };
+ if (openFileDialog.ShowDialog() == true)
+ {
+ LoadCodeplug(openFileDialog.FileName);
+
+ _settingsManager.LastCodeplugPath = openFileDialog.FileName;
+ _settingsManager.SaveSettings();
+ }
+ }
+
+ private void ResetSettings_Click(object sender, RoutedEventArgs e)
+ {
+ if (File.Exists("UserSettings.json"))
+ File.Delete("UserSettings.json");
+ }
+
+ private void LoadCodeplug(string filePath)
+ {
+ try
+ {
+ var deserializer = new DeserializerBuilder()
+ .WithNamingConvention(CamelCaseNamingConvention.Instance)
+ .IgnoreUnmatchedProperties()
+ .Build();
+
+ var yaml = File.ReadAllText(filePath);
+ Codeplug = deserializer.Deserialize(yaml);
+
+ GenerateChannelWidgets();
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"Error loading codeplug: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void GenerateChannelWidgets()
+ {
+ ChannelsCanvas.Children.Clear();
+ double offsetX = 20;
+ double offsetY = 20;
+
+ if (Codeplug != null)
+ {
+ foreach (var system in Codeplug.Systems)
+ {
+ var systemStatusBox = new SystemStatusBox(system.Name, system.Address, system.Port);
+
+ if (_settingsManager.SystemStatusPositions.TryGetValue(system.Name, out var position))
+ {
+ Canvas.SetLeft(systemStatusBox, position.X);
+ Canvas.SetTop(systemStatusBox, position.Y);
+ }
+ else
+ {
+ Canvas.SetLeft(systemStatusBox, offsetX);
+ Canvas.SetTop(systemStatusBox, offsetY);
+ }
+
+ systemStatusBox.MouseLeftButtonDown += SystemStatusBox_MouseLeftButtonDown;
+ systemStatusBox.MouseMove += SystemStatusBox_MouseMove;
+ systemStatusBox.MouseRightButtonDown += SystemStatusBox_MouseRightButtonDown;
+
+ ChannelsCanvas.Children.Add(systemStatusBox);
+
+ offsetX += 225;
+ if (offsetX + 220 > ChannelsCanvas.ActualWidth)
+ {
+ offsetX = 20;
+ offsetY += 106;
+ }
+
+ if (File.Exists(system.AliasPath))
+ system.RidAlias = AliasTools.LoadAliases(system.AliasPath);
+
+ _fneSystemManager.AddFneSystem(system.Name, system, this);
+
+ PeerSystem peer = _fneSystemManager.GetFneSystem(system.Name);
+
+ peer.peer.PeerConnected += (sender, response) =>
+ {
+ Console.WriteLine("FNE Peer connected");
+
+ Dispatcher.Invoke(() =>
+ {
+ systemStatusBox.Background = (Brush)new BrushConverter().ConvertFrom("#FF00BC48");
+ systemStatusBox.ConnectionState = "Connected";
+ });
+ };
+
+
+ peer.peer.PeerDisconnected += (response) =>
+ {
+ Console.WriteLine("FNE Peer disconnected");
+
+ Dispatcher.Invoke(() =>
+ {
+ systemStatusBox.Background = new SolidColorBrush(Colors.Red);
+ systemStatusBox.ConnectionState = "Disconnected";
+ });
+ };
+
+ Task.Run(() =>
+ {
+ peer.Start();
+ });
+
+ if (!_settingsManager.ShowSystemStatus)
+ systemStatusBox.Visibility = Visibility.Collapsed;
+
+ }
+ }
+
+ if (_settingsManager.ShowChannels && Codeplug != null)
+ {
+ foreach (var zone in Codeplug.Zones)
+ {
+ foreach (var channel in zone.Channels)
+ {
+ var channelBox = new ChannelBox(_selectedChannelsManager, _audioManager, channel.Name, channel.System, channel.Tgid);
+
+ //channelBox.crypter.AddKey(channel.GetKeyId(), channel.GetAlgoId(), channel.GetEncryptionKey());
+
+ systemStatuses.Add(channel.Name, new SlotStatus());
+
+ if (_settingsManager.ChannelPositions.TryGetValue(channel.Name, out var position))
+ {
+ Canvas.SetLeft(channelBox, position.X);
+ Canvas.SetTop(channelBox, position.Y);
+ }
+ else
+ {
+ Canvas.SetLeft(channelBox, offsetX);
+ Canvas.SetTop(channelBox, offsetY);
+ }
+
+ channelBox.PTTButtonClicked += ChannelBox_PTTButtonClicked;
+ channelBox.PageButtonClicked += ChannelBox_PageButtonClicked;
+ channelBox.HoldChannelButtonClicked += ChannelBox_HoldChannelButtonClicked;
+
+ channelBox.MouseLeftButtonDown += ChannelBox_MouseLeftButtonDown;
+ channelBox.MouseMove += ChannelBox_MouseMove;
+ channelBox.MouseRightButtonDown += ChannelBox_MouseRightButtonDown;
+ ChannelsCanvas.Children.Add(channelBox);
+
+ offsetX += 225;
+
+ if (offsetX + 220 > ChannelsCanvas.ActualWidth)
+ {
+ offsetX = 20;
+ offsetY += 106;
+ }
+ }
+ }
+ }
+
+ if (_settingsManager.ShowAlertTones && Codeplug != null)
+ {
+ foreach (var alertPath in _settingsManager.AlertToneFilePaths)
+ {
+ var alertTone = new AlertTone(alertPath)
+ {
+ IsEditMode = isEditMode
+ };
+
+ alertTone.OnAlertTone += SendAlertTone;
+
+ if (_settingsManager.AlertTonePositions.TryGetValue(alertPath, out var position))
+ {
+ Canvas.SetLeft(alertTone, position.X);
+ Canvas.SetTop(alertTone, position.Y);
+ }
+ else
+ {
+ Canvas.SetLeft(alertTone, 20);
+ Canvas.SetTop(alertTone, 20);
+ }
+
+ alertTone.MouseRightButtonUp += AlertTone_MouseRightButtonUp;
+
+ ChannelsCanvas.Children.Add(alertTone);
+ }
+ }
+
+ playbackChannelBox = new ChannelBox(_selectedChannelsManager, _audioManager, PLAYBACKCHNAME, PLAYBACKSYS, PLAYBACKTG);
+
+ if (_settingsManager.ChannelPositions.TryGetValue(PLAYBACKCHNAME, out var pos))
+ {
+ Canvas.SetLeft(playbackChannelBox, pos.X);
+ Canvas.SetTop(playbackChannelBox, pos.Y);
+ }
+ else
+ {
+ Canvas.SetLeft(playbackChannelBox, offsetX);
+ Canvas.SetTop(playbackChannelBox, offsetY);
+ }
+
+ playbackChannelBox.PTTButtonClicked += ChannelBox_PTTButtonClicked;
+ playbackChannelBox.PageButtonClicked += ChannelBox_PageButtonClicked;
+ playbackChannelBox.HoldChannelButtonClicked += ChannelBox_HoldChannelButtonClicked;
+
+ playbackChannelBox.MouseLeftButtonDown += ChannelBox_MouseLeftButtonDown;
+ playbackChannelBox.MouseMove += ChannelBox_MouseMove;
+ playbackChannelBox.MouseRightButtonDown += ChannelBox_MouseRightButtonDown;
+ ChannelsCanvas.Children.Add(playbackChannelBox);
+
+ //offsetX += 225;
+
+ //if (offsetX + 220 > ChannelsCanvas.ActualWidth)
+ //{
+ // offsetX = 20;
+ // offsetY += 106;
+ //}
+
+ AdjustCanvasHeight();
+ }
+
+ private void WaveIn_RecordingStopped(object sender, EventArgs e)
+ {
+ /* stub */
+ }
+
+ private void WaveIn_DataAvailable(object sender, WaveInEventArgs e)
+ {
+ bool isAnyTgOn = false;
+
+ foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
+ {
+ if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
+ {
+ playbackChannelBox.IsReceiving = true;
+ continue;
+ }
+
+ Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
+ Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
+
+ Task.Run(() =>
+ {
+ PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
+
+ if (channel.IsSelected && channel.PttState)
+ {
+ isAnyTgOn = true;
+
+ int samples = 320;
+
+ channel.chunkedPcm = AudioConverter.SplitToChunks(e.Buffer);
+
+ foreach (byte[] chunk in channel.chunkedPcm)
+ {
+ if (chunk.Length == samples)
+ {
+ P25EncodeAudioFrame(chunk, handler, channel, cpgChannel, system);
+ }
+ else
+ {
+ Console.WriteLine("bad sample length: " + chunk.Length);
+ }
+ }
+ }
+ });
+ }
+
+ if (isAnyTgOn && playbackChannelBox.IsSelected)
+ _audioManager.AddTalkgroupStream(PLAYBACKTG, e.Buffer);
+ }
+
+ private void SelectedChannelsChanged()
+ {
+ foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
+ {
+ if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
+ continue;
+
+ Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
+ Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
+
+ PeerSystem fne = _fneSystemManager.GetFneSystem(system.Name);
+
+ if (channel.IsSelected)
+ {
+ uint newTgid = UInt32.Parse(cpgChannel.Tgid);
+
+ if (cpgChannel.GetAlgoId() != 0 && cpgChannel.GetKeyId() != 0)
+ fne.peer.SendMasterKeyRequest(cpgChannel.GetAlgoId(), cpgChannel.GetKeyId());
+ }
+ }
+ }
+
+ private void AudioSettings_Click(object sender, RoutedEventArgs e)
+ {
+ List channels = Codeplug?.Zones.SelectMany(z => z.Channels).ToList() ?? new List();
+
+ AudioSettingsWindow audioSettingsWindow = new AudioSettingsWindow(_settingsManager, _audioManager, channels);
+ audioSettingsWindow.ShowDialog();
+ }
+
+ private void P25Page_Click(object sender, RoutedEventArgs e)
+ {
+ DigitalPageWindow pageWindow = new DigitalPageWindow(Codeplug.Systems);
+ pageWindow.Owner = this;
+ if (pageWindow.ShowDialog() == true)
+ {
+ PeerSystem handler = _fneSystemManager.GetFneSystem(pageWindow.RadioSystem.Name);
+ IOSP_CALL_ALRT callAlert = new IOSP_CALL_ALRT(UInt32.Parse(pageWindow.DstId), UInt32.Parse(pageWindow.RadioSystem.Rid));
+
+ RemoteCallData callData = new RemoteCallData
+ {
+ SrcId = UInt32.Parse(pageWindow.RadioSystem.Rid),
+ DstId = UInt32.Parse(pageWindow.DstId),
+ LCO = P25Defines.TSBK_IOSP_CALL_ALRT
+ };
+
+ byte[] tsbk = new byte[P25Defines.P25_TSBK_LENGTH_BYTES];
+ byte[] payload = new byte[P25Defines.P25_TSBK_LENGTH_BYTES];
+
+ callAlert.Encode(ref tsbk, ref payload, true, true);
+
+ handler.SendP25TSBK(callData, tsbk);
+
+ Console.WriteLine("sent page");
+ }
+ }
+
+ private async void ManualPage_Click(object sender, RoutedEventArgs e)
+ {
+ QuickCallPage pageWindow = new QuickCallPage();
+ pageWindow.Owner = this;
+ if (pageWindow.ShowDialog() == true)
+ {
+ foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
+ {
+ Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
+ Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
+ PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
+
+ if (channel.PageState)
+ {
+ ToneGenerator generator = new ToneGenerator();
+
+ double toneADuration = 1.0;
+ double toneBDuration = 3.0;
+
+ byte[] toneA = generator.GenerateTone(Double.Parse(pageWindow.ToneA), toneADuration);
+ byte[] toneB = generator.GenerateTone(Double.Parse(pageWindow.ToneB), toneBDuration);
+
+ byte[] combinedAudio = new byte[toneA.Length + toneB.Length];
+ Buffer.BlockCopy(toneA, 0, combinedAudio, 0, toneA.Length);
+ Buffer.BlockCopy(toneB, 0, combinedAudio, toneA.Length, toneB.Length);
+
+ int chunkSize = 320;
+
+ int totalChunks = (combinedAudio.Length + chunkSize - 1) / chunkSize;
+
+ Task.Run(() =>
+ {
+ //_waveProvider.ClearBuffer();
+ _audioManager.AddTalkgroupStream(cpgChannel.Tgid, combinedAudio);
+ });
+
+ await Task.Run(() =>
+ {
+ for (int i = 0; i < totalChunks; i++)
+ {
+ int offset = i * chunkSize;
+ int size = Math.Min(chunkSize, combinedAudio.Length - offset);
+
+ byte[] chunk = new byte[chunkSize];
+ Buffer.BlockCopy(combinedAudio, offset, chunk, 0, size);
+
+ if (chunk.Length == 320)
+ {
+ P25EncodeAudioFrame(chunk, handler, channel, cpgChannel, system);
+ }
+ }
+ });
+
+ double totalDurationMs = (toneADuration + toneBDuration) * 1000 + 750;
+ await Task.Delay((int)totalDurationMs + 4000);
+
+ handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), false);
+
+ Dispatcher.Invoke(() =>
+ {
+ //channel.PageState = false; // TODO: Investigate
+ channel.PageSelectButton.Background = channel.grayGradient;
+ });
+ }
+ }
+ }
+ }
+
+ private void SendAlertTone(AlertTone e)
+ {
+ Task.Run(() => SendAlertTone(e.AlertFilePath));
+ }
+
+ private void SendAlertTone(string filePath, bool forHold = false)
+ {
+ if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath))
+ {
+ try
+ {
+ foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
+ {
+ if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
+ continue;
+
+ Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
+ Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
+ PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
+
+ if (channel.PageState || (forHold && channel.HoldState))
+ {
+ byte[] pcmData;
+
+ Task.Run(async () => {
+ using (var waveReader = new WaveFileReader(filePath))
+ {
+ if (waveReader.WaveFormat.Encoding != WaveFormatEncoding.Pcm ||
+ waveReader.WaveFormat.SampleRate != 8000 ||
+ waveReader.WaveFormat.BitsPerSample != 16 ||
+ waveReader.WaveFormat.Channels != 1)
+ {
+ MessageBox.Show("The alert tone must be PCM 16-bit, Mono, 8000Hz format.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ return;
+ }
+
+ using (MemoryStream ms = new MemoryStream())
+ {
+ waveReader.CopyTo(ms);
+ pcmData = ms.ToArray();
+ }
+ }
+
+ int chunkSize = 1600;
+ int totalChunks = (pcmData.Length + chunkSize - 1) / chunkSize;
+
+ if (pcmData.Length % chunkSize != 0)
+ {
+ byte[] paddedData = new byte[totalChunks * chunkSize];
+ Buffer.BlockCopy(pcmData, 0, paddedData, 0, pcmData.Length);
+ pcmData = paddedData;
+ }
+
+ Task.Run(() =>
+ {
+ _audioManager.AddTalkgroupStream(cpgChannel.Tgid, pcmData);
+ });
+
+ DateTime startTime = DateTime.UtcNow;
+
+ for (int i = 0; i < totalChunks; i++)
+ {
+ int offset = i * chunkSize;
+ byte[] chunk = new byte[chunkSize];
+ Buffer.BlockCopy(pcmData, offset, chunk, 0, chunkSize);
+
+ PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
+
+ channel.chunkedPcm = AudioConverter.SplitToChunks(chunk);
+
+ foreach (byte[] smallchunk in channel.chunkedPcm)
+ {
+ if (smallchunk.Length == 320)
+ {
+ P25EncodeAudioFrame(smallchunk, handler, channel, cpgChannel, system);
+ }
+ }
+
+ DateTime nextPacketTime = startTime.AddMilliseconds((i + 1) * 100);
+ TimeSpan waitTime = nextPacketTime - DateTime.UtcNow;
+
+ if (waitTime.TotalMilliseconds > 0)
+ {
+ await Task.Delay(waitTime);
+ }
+ }
+
+ double totalDurationMs = ((double)pcmData.Length / 16000) + 250;
+ await Task.Delay((int)totalDurationMs + 3000);
+
+ handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), false);
+
+ Dispatcher.Invoke(() =>
+ {
+ if (forHold)
+ channel.PttButton.Background = channel.grayGradient;
+ else
+ channel.PageState = false;
+ });
+ });
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"Failed to process alert tone: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+ else
+ {
+ MessageBox.Show("Alert file not set or file not found.", "Alert", MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+ }
+
+ private void SelectWidgets_Click(object sender, RoutedEventArgs e)
+ {
+ WidgetSelectionWindow widgetSelectionWindow = new WidgetSelectionWindow();
+ widgetSelectionWindow.Owner = this;
+ if (widgetSelectionWindow.ShowDialog() == true)
+ {
+ _settingsManager.ShowSystemStatus = widgetSelectionWindow.ShowSystemStatus;
+ _settingsManager.ShowChannels = widgetSelectionWindow.ShowChannels;
+ _settingsManager.ShowAlertTones = widgetSelectionWindow.ShowAlertTones;
+
+ GenerateChannelWidgets();
+ _settingsManager.SaveSettings();
+ }
+ }
+
+ private void HandleEmergency(string dstId, string srcId)
+ {
+ bool forUs = false;
+
+ foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
+ {
+ Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
+ Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
+
+ if (dstId == cpgChannel.Tgid)
+ {
+ forUs = true;
+ channel.Emergency = true;
+ channel.LastSrcId = srcId;
+ }
+ }
+
+ if (forUs)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ _flashingManager.Start();
+ _emergencyAlertPlayback.Start();
+ });
+ }
+ }
+
+ private void ChannelBox_HoldChannelButtonClicked(object sender, ChannelBox e)
+ {
+ if (e.SystemName == PLAYBACKSYS || e.ChannelName == PLAYBACKCHNAME || e.DstId == PLAYBACKTG)
+ return;
+ }
+
+ private void ChannelBox_PageButtonClicked(object sender, ChannelBox e)
+ {
+ if (e.SystemName == PLAYBACKSYS || e.ChannelName == PLAYBACKCHNAME || e.DstId == PLAYBACKTG)
+ return;
+
+ Codeplug.System system = Codeplug.GetSystemForChannel(e.ChannelName);
+ Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(e.ChannelName);
+ PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
+
+ if (e.PageState)
+ {
+ handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), true);
+ }
+ else
+ {
+ handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), false);
+ }
+ }
+
+ private void ChannelBox_PTTButtonClicked(object sender, ChannelBox e)
+ {
+ if (e.SystemName == PLAYBACKSYS || e.ChannelName == PLAYBACKCHNAME || e.DstId == PLAYBACKTG)
+ return;
+
+ Codeplug.System system = Codeplug.GetSystemForChannel(e.ChannelName);
+ Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(e.ChannelName);
+ PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
+
+ if (!e.IsSelected)
+ return;
+
+ FneUtils.Memset(e.mi, 0x00, P25Defines.P25_MI_LENGTH);
+
+ uint srcId = UInt32.Parse(system.Rid);
+ uint dstId = UInt32.Parse(cpgChannel.Tgid);
+
+ if (e.PttState)
+ {
+ e.txStreamId = handler.NewStreamId();
+
+ handler.SendP25TDU(srcId, dstId, true);
+ }
+ else
+ {
+ handler.SendP25TDU(srcId, dstId, false);
+ }
+ }
+
+ private void ChannelBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ if (!isEditMode || !(sender is UIElement element)) return;
+
+ _draggedElement = element;
+ _startPoint = e.GetPosition(ChannelsCanvas);
+ _offsetX = _startPoint.X - Canvas.GetLeft(_draggedElement);
+ _offsetY = _startPoint.Y - Canvas.GetTop(_draggedElement);
+ _isDragging = true;
+
+ element.CaptureMouse();
+ }
+
+ private void ChannelBox_MouseMove(object sender, MouseEventArgs e)
+ {
+ if (!isEditMode || !_isDragging || _draggedElement == null) return;
+
+ Point currentPosition = e.GetPosition(ChannelsCanvas);
+
+ // Calculate the new position with snapping to the grid
+ double newLeft = Math.Round((currentPosition.X - _offsetX) / GridSize) * GridSize;
+ double newTop = Math.Round((currentPosition.Y - _offsetY) / GridSize) * GridSize;
+
+ // Ensure the box stays within canvas bounds
+ newLeft = Math.Max(0, Math.Min(newLeft, ChannelsCanvas.ActualWidth - _draggedElement.RenderSize.Width));
+ newTop = Math.Max(0, Math.Min(newTop, ChannelsCanvas.ActualHeight - _draggedElement.RenderSize.Height));
+
+ // Apply snapped position
+ Canvas.SetLeft(_draggedElement, newLeft);
+ Canvas.SetTop(_draggedElement, newTop);
+
+ // Save the new position if it's a ChannelBox
+ if (_draggedElement is ChannelBox channelBox)
+ {
+ _settingsManager.UpdateChannelPosition(channelBox.ChannelName, newLeft, newTop);
+ }
+
+ AdjustCanvasHeight();
+ }
+
+ private void ChannelBox_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ if (!isEditMode || !_isDragging || _draggedElement == null) return;
+
+ _isDragging = false;
+ _draggedElement.ReleaseMouseCapture();
+ _draggedElement = null;
+ }
+
+ private void SystemStatusBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) => ChannelBox_MouseLeftButtonDown(sender, e);
+ private void SystemStatusBox_MouseMove(object sender, MouseEventArgs e) => ChannelBox_MouseMove(sender, e);
+
+ private void SystemStatusBox_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ if (!isEditMode) return;
+
+ if (sender is SystemStatusBox systemStatusBox)
+ {
+ double x = Canvas.GetLeft(systemStatusBox);
+ double y = Canvas.GetTop(systemStatusBox);
+ _settingsManager.SystemStatusPositions[systemStatusBox.SystemName] = new ChannelPosition { X = x, Y = y };
+
+ ChannelBox_MouseRightButtonDown(sender, e);
+
+ AdjustCanvasHeight();
+ }
+ }
+
+ private void ToggleEditMode_Click(object sender, RoutedEventArgs e)
+ {
+ isEditMode = !isEditMode;
+ var menuItem = (MenuItem)sender;
+ menuItem.Header = isEditMode ? "Disable Edit Mode" : "Enable Edit Mode";
+ UpdateEditModeForWidgets();
+ }
+
+ private void UpdateEditModeForWidgets()
+ {
+ foreach (var child in ChannelsCanvas.Children)
+ {
+ if (child is AlertTone alertTone)
+ {
+ alertTone.IsEditMode = isEditMode;
+ }
+
+ if (child is ChannelBox channelBox)
+ {
+ channelBox.IsEditMode = isEditMode;
+ }
+ }
+ }
+
+ private void AddAlertTone_Click(object sender, RoutedEventArgs e)
+ {
+ OpenFileDialog openFileDialog = new OpenFileDialog
+ {
+ Filter = "WAV Files (*.wav)|*.wav|All Files (*.*)|*.*",
+ Title = "Select Alert Tone"
+ };
+
+ if (openFileDialog.ShowDialog() == true)
+ {
+ string alertFilePath = openFileDialog.FileName;
+ var alertTone = new AlertTone(alertFilePath)
+ {
+ IsEditMode = isEditMode
+ };
+
+ alertTone.OnAlertTone += SendAlertTone;
+
+ if (_settingsManager.AlertTonePositions.TryGetValue(alertFilePath, out var position))
+ {
+ Canvas.SetLeft(alertTone, position.X);
+ Canvas.SetTop(alertTone, position.Y);
+ }
+ else
+ {
+ Canvas.SetLeft(alertTone, 20);
+ Canvas.SetTop(alertTone, 20);
+ }
+
+ alertTone.MouseRightButtonUp += AlertTone_MouseRightButtonUp;
+
+ ChannelsCanvas.Children.Add(alertTone);
+ _settingsManager.UpdateAlertTonePaths(alertFilePath);
+
+ AdjustCanvasHeight();
+ }
+ }
+
+ private void AlertTone_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
+ {
+ if (!isEditMode) return;
+
+ if (sender is AlertTone alertTone)
+ {
+ double x = Canvas.GetLeft(alertTone);
+ double y = Canvas.GetTop(alertTone);
+ _settingsManager.UpdateAlertTonePosition(alertTone.AlertFilePath, x, y);
+
+ AdjustCanvasHeight();
+ }
+ }
+
+ private void AdjustCanvasHeight()
+ {
+ double maxBottom = 0;
+
+ foreach (UIElement child in ChannelsCanvas.Children)
+ {
+ double childBottom = Canvas.GetTop(child) + child.RenderSize.Height;
+ if (childBottom > maxBottom)
+ {
+ maxBottom = childBottom;
+ }
+ }
+
+ ChannelsCanvas.Height = maxBottom + 150;
+ }
+
+ private void MainWindow_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (!string.IsNullOrEmpty(_settingsManager.LastCodeplugPath) && File.Exists(_settingsManager.LastCodeplugPath))
+ {
+ LoadCodeplug(_settingsManager.LastCodeplugPath);
+ }
+ else
+ {
+ GenerateChannelWidgets();
+ }
+ }
+
+ private async void OnHoldTimerElapsed(object sender, ElapsedEventArgs e)
+ {
+ foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
+ {
+ if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
+ continue;
+
+ Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
+ Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
+ PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
+
+ if (channel.HoldState && !channel.IsReceiving && !channel.PttState && !channel.PageState)
+ {
+ handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), true);
+ await Task.Delay(1000);
+
+ SendAlertTone("hold.wav", true);
+ }
+ }
+ }
+
+ protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
+ {
+ _settingsManager.SaveSettings();
+ base.OnClosing(e);
+ Application.Current.Shutdown();
+ }
+
+ private void ClearEmergency_Click(object sender, RoutedEventArgs e)
+ {
+ _emergencyAlertPlayback.Stop();
+ _flashingManager.Stop();
+
+ foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
+ {
+ channel.Emergency = false;
+ }
+ }
+
+ private void btnAlert1_Click(object sender, RoutedEventArgs e)
+ {
+ Dispatcher.Invoke(() => {
+ SendAlertTone("alert1.wav");
+ });
+ }
+
+ private void btnAlert2_Click(object sender, RoutedEventArgs e)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ SendAlertTone("alert2.wav");
+ });
+ }
+
+ private void btnAlert3_Click(object sender, RoutedEventArgs e)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ SendAlertTone("alert3.wav");
+ });
+ }
+
+ private async void btnGlobalPtt_Click(object sender, RoutedEventArgs e)
+ {
+ if (globalPttState)
+ await Task.Delay(500);
+
+ globalPttState = !globalPttState;
+
+ foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
+ {
+ if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
+ continue;
+
+ Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
+ Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
+ PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
+
+ channel.txStreamId = handler.NewStreamId();
+
+ if (globalPttState)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ btnGlobalPtt.Background = channel.redGradient;
+ channel.PttState = true;
+ });
+
+ handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), true);
+ }
+ else
+ {
+ Dispatcher.Invoke(() =>
+ {
+ btnGlobalPtt.Background = channel.grayGradient;
+ channel.PttState = false;
+ });
+
+ handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), false);
+ }
+ }
+ }
+
+ private void SelectAll_Click(object sender, RoutedEventArgs e)
+ {
+ foreach (ChannelBox channel in ChannelsCanvas.Children.OfType())
+ {
+ if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
+ continue;
+
+ Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
+ Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
+
+ if (!channel.IsSelected)
+ {
+ channel.IsSelected = true;
+
+ channel.Background = channel.IsSelected ? (Brush)new BrushConverter().ConvertFrom("#FF0B004B") : Brushes.Gray;
+
+ if (channel.IsSelected)
+ {
+ _selectedChannelsManager.AddSelectedChannel(channel);
+ }
+ else
+ {
+ _selectedChannelsManager.RemoveSelectedChannel(channel);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Helper to encode and transmit PCM audio as P25 IMBE frames.
+ ///
+ private void P25EncodeAudioFrame(byte[] pcm, PeerSystem handler, ChannelBox channel, Codeplug.Channel cpgChannel, Codeplug.System system)
+ {
+ bool encryptCall = true; // TODO: make this dynamic somewhere?
+
+ if (channel.p25N > 17)
+ channel.p25N = 0;
+ if (channel.p25N == 0)
+ FneUtils.Memset(channel.netLDU1, 0, 9 * 25);
+ if (channel.p25N == 9)
+ FneUtils.Memset(channel.netLDU2, 0, 9 * 25);
+
+ // Log.Logger.Debug($"BYTE BUFFER {FneUtils.HexDump(pcm)}");
+
+ //// pre-process: apply gain to PCM audio frames
+ //if (Program.Configuration.TxAudioGain != 1.0f)
+ //{
+ // BufferedWaveProvider buffer = new BufferedWaveProvider(waveFormat);
+ // buffer.AddSamples(pcm, 0, pcm.Length);
+
+ // VolumeWaveProvider16 gainControl = new VolumeWaveProvider16(buffer);
+ // gainControl.Volume = Program.Configuration.TxAudioGain;
+ // gainControl.Read(pcm, 0, pcm.Length);
+ //}
+
+ int smpIdx = 0;
+ short[] samples = new short[FneSystemBase.MBE_SAMPLES_LENGTH];
+ for (int pcmIdx = 0; pcmIdx < pcm.Length; pcmIdx += 2)
+ {
+ samples[smpIdx] = (short)((pcm[pcmIdx + 1] << 8) + pcm[pcmIdx + 0]);
+ smpIdx++;
+ }
+
+ // Convert to floats
+ float[] fSamples = AudioConverter.PcmToFloat(samples);
+
+ // Convert to signal
+ DiscreteSignal signal = new DiscreteSignal(8000, fSamples, true);
+
+ // Log.Logger.Debug($"SAMPLE BUFFER {FneUtils.HexDump(samples)}");
+
+ // encode PCM samples into IMBE codewords
+ byte[] imbe = new byte[FneSystemBase.IMBE_BUF_LEN];
+
+
+ int tone = 0;
+
+ if (true) // TODO: Disable/enable detection
+ {
+ tone = channel.toneDetector.Detect(signal);
+ }
+ if (tone > 0)
+ {
+ MBEToneGenerator.IMBEEncodeSingleTone((ushort)tone, imbe);
+ Console.WriteLine($"({system.Name}) P25D: {tone} HZ TONE DETECT");
+ }
+ else
+ {
+#if WIN32
+ if (channel.extFullRateVocoder == null)
+ channel.extFullRateVocoder = new AmbeVocoder(true);
+
+ channel.extFullRateVocoder.encode(samples, out imbe);
+#else
+ if (channel.encoder == null)
+ channel.encoder = new MBEEncoder(MBE_MODE.IMBE_88BIT);
+
+ channel.encoder.encode(samples, imbe);
+#endif
+ }
+ // Log.Logger.Debug($"IMBE {FneUtils.HexDump(imbe)}");
+
+ if (encryptCall && cpgChannel.GetAlgoId() != 0 && cpgChannel.GetKeyId() != 0)
+ {
+ // initial HDU MI
+ if (channel.p25N == 0)
+ {
+ if (channel.mi.All(b => b == 0))
+ {
+ Random random = new Random();
+
+ for (int i = 0; i < P25Defines.P25_MI_LENGTH; i++)
+ {
+ channel.mi[i] = (byte)random.Next(0x00, 0x100);
+ }
+ }
+
+ channel.crypter.Prepare(cpgChannel.GetAlgoId(), cpgChannel.GetKeyId(), ProtocolType.P25Phase1, channel.mi);
+ }
+
+ // crypto time
+ channel.crypter.Process(imbe, channel.p25N < 9U ? P25Crypto.FrameType.LDU1 : P25Crypto.FrameType.LDU2, 0);
+
+ // last block of LDU2, prepare a new MI
+ if (channel.p25N == 17U)
+ {
+ P25Crypto.CycleP25Lfsr(channel.mi);
+ channel.crypter.Prepare(cpgChannel.GetAlgoId(), cpgChannel.GetKeyId(), ProtocolType.P25Phase1, channel.mi);
+ }
+ }
+
+ // fill the LDU buffers appropriately
+ switch (channel.p25N)
+ {
+ // LDU1
+ case 0:
+ Buffer.BlockCopy(imbe, 0, channel.netLDU1, 10, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 1:
+ Buffer.BlockCopy(imbe, 0, channel.netLDU1, 26, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 2:
+ Buffer.BlockCopy(imbe, 0, channel.netLDU1, 55, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 3:
+ Buffer.BlockCopy(imbe, 0, channel.netLDU1, 80, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 4:
+ Buffer.BlockCopy(imbe, 0, channel.netLDU1, 105, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 5:
+ Buffer.BlockCopy(imbe, 0, channel.netLDU1, 130, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 6:
+ Buffer.BlockCopy(imbe, 0, channel.netLDU1, 155, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 7:
+ Buffer.BlockCopy(imbe, 0, channel.netLDU1, 180, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 8:
+ Buffer.BlockCopy(imbe, 0, channel.netLDU1, 204, FneSystemBase.IMBE_BUF_LEN);
+ break;
+
+ // LDU2
+ case 9:
+ Buffer.BlockCopy(imbe, 0, channel.netLDU2, 10, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 10:
+ Buffer.BlockCopy(imbe, 0, channel.netLDU2, 26, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 11:
+ Buffer.BlockCopy(imbe, 0, channel.netLDU2, 55, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 12:
+ Buffer.BlockCopy(imbe, 0, channel.netLDU2, 80, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 13:
+ Buffer.BlockCopy(imbe, 0, channel.netLDU2, 105, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 14:
+ Buffer.BlockCopy(imbe, 0, channel.netLDU2, 130, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 15:
+ Buffer.BlockCopy(imbe, 0, channel.netLDU2, 155, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 16:
+ Buffer.BlockCopy(imbe, 0, channel.netLDU2, 180, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 17:
+ Buffer.BlockCopy(imbe, 0, channel.netLDU2, 204, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ }
+
+ uint srcId = UInt32.Parse(system.Rid);
+ uint dstId = UInt32.Parse(cpgChannel.Tgid);
+
+ FnePeer peer = handler.peer;
+ RemoteCallData callData = new RemoteCallData()
+ {
+ SrcId = srcId,
+ DstId = dstId,
+ LCO = P25Defines.LC_GROUP
+ };
+
+ // send P25 LDU1
+ if (channel.p25N == 8U)
+ {
+ ushort pktSeq = 0;
+ if (channel.p25SeqNo == 0U)
+ pktSeq = peer.pktSeq(true);
+ else
+ pktSeq = peer.pktSeq();
+
+ //Console.WriteLine($"({channel.SystemName}) P25D: Traffic *VOICE FRAME * PEER {handler.PeerId} SRC_ID {srcId} TGID {dstId} [STREAM ID {channel.txStreamId}]");
+
+ byte[] payload = new byte[200];
+ handler.CreateNewP25MessageHdr((byte)P25DUID.LDU1, callData, ref payload, cpgChannel.GetAlgoId(), cpgChannel.GetKeyId(), channel.mi);
+ handler.CreateP25LDU1Message(channel.netLDU1, ref payload, srcId, dstId);
+
+ peer.SendMaster(new Tuple(Constants.NET_FUNC_PROTOCOL, Constants.NET_PROTOCOL_SUBFUNC_P25), payload, pktSeq, channel.txStreamId);
+ }
+
+ // send P25 LDU2
+ if (channel.p25N == 17U)
+ {
+ ushort pktSeq = 0;
+ if (channel.p25SeqNo == 0U)
+ pktSeq = peer.pktSeq(true);
+ else
+ pktSeq = peer.pktSeq();
+
+ //Console.WriteLine($"({channel.SystemName}) P25D: Traffic *VOICE FRAME * PEER {handler.PeerId} SRC_ID {srcId} TGID {dstId} [STREAM ID {channel.txStreamId}]");
+
+ byte[] payload = new byte[200];
+ handler.CreateNewP25MessageHdr((byte)P25DUID.LDU2, callData, ref payload, cpgChannel.GetAlgoId(), cpgChannel.GetKeyId(), channel.mi);
+ handler.CreateP25LDU2Message(channel.netLDU2, ref payload, new CryptoParams { AlgId = cpgChannel.GetAlgoId(), KeyId = cpgChannel.GetKeyId(), Mi = channel.mi });
+
+ peer.SendMaster(new Tuple(Constants.NET_FUNC_PROTOCOL, Constants.NET_PROTOCOL_SUBFUNC_P25), payload, pktSeq, channel.txStreamId);
+ }
+
+ channel.p25SeqNo++;
+ channel.p25N++;
+ }
+
+ ///
+ /// Helper to decode and playback P25 IMBE frames as PCM audio.
+ ///
+ ///
+ ///
+ private void P25DecodeAudioFrame(byte[] ldu, P25DataReceivedEvent e, PeerSystem system, ChannelBox channel, bool emergency = false, P25Crypto.FrameType frameType = P25Crypto.FrameType.LDU1)
+ {
+ try
+ {
+ // decode 9 IMBE codewords into PCM samples
+ for (int n = 0; n < 9; n++)
+ {
+ byte[] imbe = new byte[FneSystemBase.IMBE_BUF_LEN];
+ switch (n)
+ {
+ case 0:
+ Buffer.BlockCopy(ldu, 10, imbe, 0, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 1:
+ Buffer.BlockCopy(ldu, 26, imbe, 0, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 2:
+ Buffer.BlockCopy(ldu, 55, imbe, 0, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 3:
+ Buffer.BlockCopy(ldu, 80, imbe, 0, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 4:
+ Buffer.BlockCopy(ldu, 105, imbe, 0, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 5:
+ Buffer.BlockCopy(ldu, 130, imbe, 0, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 6:
+ Buffer.BlockCopy(ldu, 155, imbe, 0, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 7:
+ Buffer.BlockCopy(ldu, 180, imbe, 0, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ case 8:
+ Buffer.BlockCopy(ldu, 204, imbe, 0, FneSystemBase.IMBE_BUF_LEN);
+ break;
+ }
+
+ //Log.Logger.Debug($"Decoding IMBE buffer: {FneUtils.HexDump(imbe)}");
+
+ short[] samples = new short[FneSystemBase.MBE_SAMPLES_LENGTH];
+
+ channel.crypter.Process(imbe, frameType, n);
+
+#if WIN32
+ if (channel.extFullRateVocoder == null)
+ channel.extFullRateVocoder = new AmbeVocoder(true);
+
+ channel.p25Errs = channel.extFullRateVocoder.decode(imbe, out samples);
+#else
+
+ channel.p25Errs = channel.decoder.decode(imbe, samples);
+#endif
+
+ if (emergency && !channel.Emergency)
+ {
+ Task.Run(() =>
+ {
+ HandleEmergency(e.SrcId.ToString(), e.DstId.ToString());
+ });
+ }
+
+ if (samples != null)
+ {
+ //Log.Logger.Debug($"({Config.Name}) P25D: Traffic *VOICE FRAME * PEER {e.PeerId} SRC_ID {e.SrcId} TGID {e.DstId} VC{n} ERRS {errs} [STREAM ID {e.StreamId}]");
+ //Log.Logger.Debug($"IMBE {FneUtils.HexDump(imbe)}");
+ //Console.WriteLine($"SAMPLE BUFFER {FneUtils.HexDump(samples)}");
+
+ int pcmIdx = 0;
+ byte[] pcmData = new byte[samples.Length * 2];
+ for (int i = 0; i < samples.Length; i++)
+ {
+ pcmData[pcmIdx] = (byte)(samples[i] & 0xFF);
+ pcmData[pcmIdx + 1] = (byte)((samples[i] >> 8) & 0xFF);
+ pcmIdx += 2;
+ }
+
+ _audioManager.AddTalkgroupStream(e.DstId.ToString(), pcmData);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Audio Decode Exception: {ex.Message}");
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public void KeyResponseReceived(KeyResponseEvent e)
+ {
+ //Console.WriteLine($"Message ID: {e.KmmKey.MessageId}");
+ //Console.WriteLine($"Decrypt Info Format: {e.KmmKey.DecryptInfoFmt}");
+ //Console.WriteLine($"Algorithm ID: {e.KmmKey.AlgId}");
+ //Console.WriteLine($"Key ID: {e.KmmKey.KeyId}");
+ //Console.WriteLine($"Keyset ID: {e.KmmKey.KeysetItem.KeysetId}");
+ //Console.WriteLine($"Keyset Alg ID: {e.KmmKey.KeysetItem.AlgId}");
+ //Console.WriteLine($"Keyset Key Length: {e.KmmKey.KeysetItem.KeyLength}");
+ //Console.WriteLine($"Number of Keys: {e.KmmKey.KeysetItem.Keys.Count}");
+
+ foreach (var key in e.KmmKey.KeysetItem.Keys)
+ {
+ //Console.WriteLine($" Key Format: {key.KeyFormat}");
+ //Console.WriteLine($" SLN: {key.Sln}");
+ //Console.WriteLine($" Key ID: {key.KeyId}");
+ //Console.WriteLine($" Key Data: {BitConverter.ToString(key.GetKey())}");
+
+ Dispatcher.Invoke(() =>
+ {
+ foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
+ {
+ Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
+ Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
+ PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
+
+ if (cpgChannel.GetKeyId() != 0 && cpgChannel.GetAlgoId() != 0)
+ channel.crypter.AddKey(key.KeyId, e.KmmKey.KeysetItem.AlgId, key.GetKey());
+ }
+ });
+ }
+ }
+
+ private void KeyStatus_Click(object sender, RoutedEventArgs e)
+ {
+ KeyStatusWindow keyStatus = new KeyStatusWindow(Codeplug, this);
+ keyStatus.Show();
+ }
+
+ ///
+ /// Event handler used to process incoming P25 data.
+ ///
+ ///
+ ///
+ public void P25DataReceived(P25DataReceivedEvent e, DateTime pktTime)
+ {
+ uint sysId = (uint)((e.Data[11U] << 8) | (e.Data[12U] << 0));
+ uint netId = FneUtils.Bytes3ToUInt32(e.Data, 16);
+ byte control = e.Data[14U];
+
+ byte len = e.Data[23];
+ byte[] data = new byte[len];
+ for (int i = 24; i < len; i++)
+ data[i - 24] = e.Data[i];
+
+ Dispatcher.Invoke(() =>
+ {
+ foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
+ {
+ Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
+ Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
+
+ bool isEmergency = false;
+ bool encrypted = false;
+
+ PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
+
+ if (!channel.IsEnabled)
+ continue;
+
+ if (cpgChannel.Tgid != e.DstId.ToString())
+ continue;
+
+ if (!systemStatuses.ContainsKey(cpgChannel.Name))
+ {
+ systemStatuses[cpgChannel.Name] = new SlotStatus();
+ }
+
+ if (channel.decoder == null)
+ {
+ channel.decoder = new MBEDecoder(MBE_MODE.IMBE_88BIT);
+ }
+
+ SlotStatus slot = systemStatuses[cpgChannel.Name];
+
+ // if this is an LDU1 see if this is the first LDU with HDU encryption data
+ if (e.DUID == P25DUID.LDU1)
+ {
+ byte frameType = e.Data[180];
+
+ // get the initial MI and other enc info (bug found by the screeeeeeeeech on initial tx...)
+ if (frameType == P25Defines.P25_FT_HDU_VALID)
+ {
+ channel.algId = e.Data[181];
+ channel.kId = (ushort)((e.Data[182] << 8) | e.Data[183]);
+ Array.Copy(e.Data, 184, channel.mi, 0, P25Defines.P25_MI_LENGTH);
+
+ channel.crypter.Prepare(channel.algId, channel.kId, P25Crypto.ProtocolType.P25Phase1, channel.mi);
+
+ encrypted = true;
+ }
+ }
+
+ // is this a new call stream?
+ if (e.StreamId != slot.RxStreamId && ((e.DUID != P25DUID.TDU) && (e.DUID != P25DUID.TDULC)))
+ {
+ channel.IsReceiving = true;
+ slot.RxStart = pktTime;
+ Console.WriteLine($"({system.Name}) P25D: Traffic *CALL START * PEER {e.PeerId} SRC_ID {e.SrcId} TGID {e.DstId} [STREAM ID {e.StreamId}]");
+
+ FneUtils.Memset(channel.mi, 0x00, P25Defines.P25_MI_LENGTH);
+
+ callHistoryWindow.AddCall(cpgChannel.Name, (int)e.SrcId, (int)e.DstId);
+ callHistoryWindow.ChannelKeyed(cpgChannel.Name, (int)e.SrcId, encrypted);
+
+ string alias = string.Empty;
+
+ try
+ {
+ alias = AliasTools.GetAliasByRid(system.RidAlias, (int)e.SrcId);
+ }
+ catch (Exception) { }
+
+ if (string.IsNullOrEmpty(alias))
+ channel.LastSrcId = "Last SRC: " + e.SrcId;
+ else
+ channel.LastSrcId = "Last: " + alias;
+
+ if (channel.algId != P25Defines.P25_ALGO_UNENCRYPT)
+ channel.Background = (Brush)new BrushConverter().ConvertFrom("#ffdeaf0a");
+ else
+ channel.Background = (Brush)new BrushConverter().ConvertFrom("#FF00BC48");
+ }
+
+ // Is the call over?
+ if (((e.DUID == P25DUID.TDU) || (e.DUID == P25DUID.TDULC)) && (slot.RxType != fnecore.FrameType.TERMINATOR))
+ {
+ channel.IsReceiving = false;
+ TimeSpan callDuration = pktTime - slot.RxStart;
+ Console.WriteLine($"({system.Name}) P25D: Traffic *CALL END * PEER {e.PeerId} SRC_ID {e.SrcId} TGID {e.DstId} DUR {callDuration} [STREAM ID {e.StreamId}]");
+ channel.Background = (Brush)new BrushConverter().ConvertFrom("#FF0B004B");
+ callHistoryWindow.ChannelUnkeyed(cpgChannel.Name, (int)e.SrcId);
+ return;
+ }
+
+ if ((channel.algId != cpgChannel.GetAlgoId() || channel.kId != cpgChannel.GetKeyId()) && channel.algId != P25Defines.P25_ALGO_UNENCRYPT)
+ continue;
+
+ byte[] newMI = new byte[P25Defines.P25_MI_LENGTH];
+
+ int count = 0;
+
+ switch (e.DUID)
+ {
+ case P25DUID.LDU1:
+ {
+ // The '62', '63', '64', '65', '66', '67', '68', '69', '6A' records are LDU1
+ if ((data[0U] == 0x62U) && (data[22U] == 0x63U) &&
+ (data[36U] == 0x64U) && (data[53U] == 0x65U) &&
+ (data[70U] == 0x66U) && (data[87U] == 0x67U) &&
+ (data[104U] == 0x68U) && (data[121U] == 0x69U) &&
+ (data[138U] == 0x6AU))
+ {
+ // The '62' record - IMBE Voice 1
+ Buffer.BlockCopy(data, count, channel.netLDU1, 0, 22);
+ count += 22;
+
+ // The '63' record - IMBE Voice 2
+ Buffer.BlockCopy(data, count, channel.netLDU1, 25, 14);
+ count += 14;
+
+ // The '64' record - IMBE Voice 3 + Link Control
+ Buffer.BlockCopy(data, count, channel.netLDU1, 50, 17);
+ byte serviceOptions = data[count + 3];
+ isEmergency = (serviceOptions & 0x80) == 0x80;
+ count += 17;
+
+ // The '65' record - IMBE Voice 4 + Link Control
+ Buffer.BlockCopy(data, count, channel.netLDU1, 75, 17);
+ count += 17;
+
+ // The '66' record - IMBE Voice 5 + Link Control
+ Buffer.BlockCopy(data, count, channel.netLDU1, 100, 17);
+ count += 17;
+
+ // The '67' record - IMBE Voice 6 + Link Control
+ Buffer.BlockCopy(data, count, channel.netLDU1, 125, 17);
+ count += 17;
+
+ // The '68' record - IMBE Voice 7 + Link Control
+ Buffer.BlockCopy(data, count, channel.netLDU1, 150, 17);
+ count += 17;
+
+ // The '69' record - IMBE Voice 8 + Link Control
+ Buffer.BlockCopy(data, count, channel.netLDU1, 175, 17);
+ count += 17;
+
+ // The '6A' record - IMBE Voice 9 + Low Speed Data
+ Buffer.BlockCopy(data, count, channel.netLDU1, 200, 16);
+ count += 16;
+
+ // decode 9 IMBE codewords into PCM samples
+ P25DecodeAudioFrame(channel.netLDU1, e, handler, channel, isEmergency);
+ }
+ }
+ break;
+ case P25DUID.LDU2:
+ {
+ // The '6B', '6C', '6D', '6E', '6F', '70', '71', '72', '73' records are LDU2
+ if ((data[0U] == 0x6BU) && (data[22U] == 0x6CU) &&
+ (data[36U] == 0x6DU) && (data[53U] == 0x6EU) &&
+ (data[70U] == 0x6FU) && (data[87U] == 0x70U) &&
+ (data[104U] == 0x71U) && (data[121U] == 0x72U) &&
+ (data[138U] == 0x73U))
+ {
+ // The '6B' record - IMBE Voice 10
+ Buffer.BlockCopy(data, count, channel.netLDU2, 0, 22);
+ count += 22;
+
+ // The '6C' record - IMBE Voice 11
+ Buffer.BlockCopy(data, count, channel.netLDU2, 25, 14);
+ count += 14;
+
+ // The '6D' record - IMBE Voice 12 + Encryption Sync
+ Buffer.BlockCopy(data, count, channel.netLDU2, 50, 17);
+ newMI[0] = data[count + 1];
+ newMI[1] = data[count + 2];
+ newMI[2] = data[count + 3];
+ count += 17;
+
+ // The '6E' record - IMBE Voice 13 + Encryption Sync
+ Buffer.BlockCopy(data, count, channel.netLDU2, 75, 17);
+ newMI[3] = data[count + 1];
+ newMI[4] = data[count + 2];
+ newMI[5] = data[count + 3];
+ count += 17;
+
+ // The '6F' record - IMBE Voice 14 + Encryption Sync
+ Buffer.BlockCopy(data, count, channel.netLDU2, 100, 17);
+ newMI[6] = data[count + 1];
+ newMI[7] = data[count + 2];
+ newMI[8] = data[count + 3];
+ count += 17;
+
+ // The '70' record - IMBE Voice 15 + Encryption Sync
+ Buffer.BlockCopy(data, count, channel.netLDU2, 125, 17);
+ channel.algId = data[count + 1]; // Algorithm ID
+ channel.kId = (ushort)((data[count + 2] << 8) | data[count + 3]); // Key ID
+ count += 17;
+
+ // The '71' record - IMBE Voice 16 + Encryption Sync
+ Buffer.BlockCopy(data, count, channel.netLDU2, 150, 17);
+ count += 17;
+
+ // The '72' record - IMBE Voice 17 + Encryption Sync
+ Buffer.BlockCopy(data, count, channel.netLDU2, 175, 17);
+ count += 17;
+
+ // The '73' record - IMBE Voice 18 + Low Speed Data
+ Buffer.BlockCopy(data, count, channel.netLDU2, 200, 16);
+ count += 16;
+
+ if (channel.p25Errs > 0) // temp, need to actually get errors I guess
+ P25Crypto.CycleP25Lfsr(channel.mi);
+ else
+ Array.Copy(newMI, channel.mi, P25Defines.P25_MI_LENGTH);
+
+ // decode 9 IMBE codewords into PCM samples
+ P25DecodeAudioFrame(channel.netLDU2, e, handler, channel, isEmergency, P25Crypto.FrameType.LDU2);
+ }
+ }
+ break;
+ }
+
+ if (channel.mi != null)
+ channel.crypter.Prepare(channel.algId, channel.kId, P25Crypto.ProtocolType.P25Phase1, channel.mi);
+
+ slot.RxRFS = e.SrcId;
+ slot.RxType = e.FrameType;
+ slot.RxTGId = e.DstId;
+ slot.RxTime = pktTime;
+ slot.RxStreamId = e.StreamId;
+
+ }
+ });
+ }
+
+ private void CallHist_Click(object sender, RoutedEventArgs e)
+ {
+ callHistoryWindow.Show();
+ }
+ }
+}
diff --git a/WhackerLinkConsoleV2/P25Crypto.cs b/DVMConsole/P25Crypto.cs
similarity index 92%
rename from WhackerLinkConsoleV2/P25Crypto.cs
rename to DVMConsole/P25Crypto.cs
index 2005142..7cf41ff 100644
--- a/WhackerLinkConsoleV2/P25Crypto.cs
+++ b/DVMConsole/P25Crypto.cs
@@ -1,32 +1,24 @@
-/*
-* WhackerLink - WhackerLinkConsoleV2
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - DVMConsole
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, either version 3 of the License, or
-* (at your option) any later version.
+* @package DVM / DVM Console
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
+* Copyright (C) 2025 Caleb, K4PHP
*
-* You should have received a copy of the GNU General Public License
-* along with this program. If not, see .
-*
-* Derrived from https://github.com/boatbod/op25/op25/gr-op25_repeater/lib/p25_crypt_algs.cc
-* Derrived from https://github.com/boatbod/op25/blob/master/op25/gr-op25_repeater/lib/op25_crypt_aes.cc
-*
-* Copyright (C) 2025 Caleb, K4PHP
-*
*/
+// TODO: Move to fnecore
+
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Linq;
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
public class P25Crypto
{
diff --git a/WhackerLinkConsoleV2/PeerSystem.cs b/DVMConsole/PeerSystem.cs
similarity index 78%
rename from WhackerLinkConsoleV2/PeerSystem.cs
rename to DVMConsole/PeerSystem.cs
index 13a0968..5f32380 100644
--- a/WhackerLinkConsoleV2/PeerSystem.cs
+++ b/DVMConsole/PeerSystem.cs
@@ -1,31 +1,22 @@
// SPDX-License-Identifier: AGPL-3.0-only
/**
-* Digital Voice Modem - Audio Bridge
+* Digital Voice Modem - DVMConsole
* AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
-* @package DVM / Audio Bridge
+* @package DVM / DVM Console
* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
* Copyright (C) 2023 Bryan Biedenkapp, N2PLL
-* Copyright (C) 2024 Caleb, KO4UYJ
+* Copyright (C) 2024-2025 Caleb, K4PHP
*
*/
-using System;
-using System.Net;
-using System.Collections.Generic;
-using System.Text;
-using System.Net.Sockets;
-using System.Threading.Tasks;
-using Serilog;
+using System.Net;
using fnecore;
-using WhackerLinkLib.Models.Radio;
-using static WhackerLinkLib.Models.Radio.Codeplug;
-using Microsoft.AspNetCore.Mvc.Formatters;
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
///
/// Implements a peer FNE router system.
@@ -67,12 +58,12 @@ namespace WhackerLinkConsoleV2
}
catch (FormatException)
{
- IPAddress[] addresses = Dns.GetHostAddresses("fne.zone1.scan.stream");
+ IPAddress[] addresses = Dns.GetHostAddresses(system.Address);
if (addresses.Length > 0)
endpoint = new IPEndPoint(addresses[0], system.Port);
}
- FnePeer peer = new FnePeer("WLINKCONSOLE", system.PeerId, endpoint, presharedKey);
+ FnePeer peer = new FnePeer("DVMCONSOLE", system.PeerId, endpoint, presharedKey);
// set configuration parameters
peer.Passphrase = system.AuthKey;
@@ -81,8 +72,8 @@ namespace WhackerLinkConsoleV2
Details = new PeerDetails
{
ConventionalPeer = true,
- Software = "WLINKCONSOLE",
- Identity = "CONS OP"
+ Software = "DVMCONSOLE",
+ Identity = "CONS OP" // TODO: Add to config
}
};
@@ -100,8 +91,7 @@ namespace WhackerLinkConsoleV2
///
private static void Peer_PeerConnected(object sender, PeerConnectedEvent e)
{
- //FnePeer peer = (FnePeer)sender;
- //peer.SendMasterGroupAffiliation(1, (uint)Program.Configuration.DestinationId);
+ /* stub */
}
///
diff --git a/WhackerLinkConsoleV2/QuickCallPage.xaml b/DVMConsole/QuickCallPage.xaml
similarity index 90%
rename from WhackerLinkConsoleV2/QuickCallPage.xaml
rename to DVMConsole/QuickCallPage.xaml
index e7a4151..e563b43 100644
--- a/WhackerLinkConsoleV2/QuickCallPage.xaml
+++ b/DVMConsole/QuickCallPage.xaml
@@ -1,9 +1,9 @@
-
diff --git a/WhackerLinkConsoleV2/QuickCallPage.xaml.cs b/DVMConsole/QuickCallPage.xaml.cs
similarity index 94%
rename from WhackerLinkConsoleV2/QuickCallPage.xaml.cs
rename to DVMConsole/QuickCallPage.xaml.cs
index 2bdcb48..ff2b286 100644
--- a/WhackerLinkConsoleV2/QuickCallPage.xaml.cs
+++ b/DVMConsole/QuickCallPage.xaml.cs
@@ -1,5 +1,5 @@
/*
-* WhackerLink - WhackerLinkConsoleV2
+* WhackerLink - DVMConsole
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,7 +20,7 @@
using System.Windows;
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
///
/// Interaction logic for QuickCallPage.xaml
diff --git a/WhackerLinkConsoleV2/SampleTimeConvert.cs b/DVMConsole/SampleTimeConvert.cs
similarity index 97%
rename from WhackerLinkConsoleV2/SampleTimeConvert.cs
rename to DVMConsole/SampleTimeConvert.cs
index 56a6007..65109d1 100644
--- a/WhackerLinkConsoleV2/SampleTimeConvert.cs
+++ b/DVMConsole/SampleTimeConvert.cs
@@ -10,11 +10,10 @@
* Copyright (C) 2023 Bryan Biedenkapp, N2PLL
*
*/
-using System;
using NAudio.Wave;
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
///
///
diff --git a/WhackerLinkConsoleV2/SelectedChannelsManager.cs b/DVMConsole/SelectedChannelsManager.cs
similarity index 60%
rename from WhackerLinkConsoleV2/SelectedChannelsManager.cs
rename to DVMConsole/SelectedChannelsManager.cs
index 82f7e3c..11203ee 100644
--- a/WhackerLinkConsoleV2/SelectedChannelsManager.cs
+++ b/DVMConsole/SelectedChannelsManager.cs
@@ -1,26 +1,19 @@
-/*
-* WhackerLink - WhackerLinkConsoleV2
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - DVMConsole
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, either version 3 of the License, or
-* (at your option) any later version.
+* @package DVM / DVM Console
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
+* Copyright (C) 2024 Caleb, K4PHP
*
-* You should have received a copy of the GNU General Public License
-* along with this program. If not, see .
-*
-* Copyright (C) 2024 Caleb, K4PHP
-*
*/
-using WhackerLinkConsoleV2.Controls;
+using DVMConsole.Controls;
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
public class SelectedChannelsManager
{
diff --git a/WhackerLinkConsoleV2/SettingsManager.cs b/DVMConsole/SettingsManager.cs
similarity index 81%
rename from WhackerLinkConsoleV2/SettingsManager.cs
rename to DVMConsole/SettingsManager.cs
index 96690b9..a7e83d6 100644
--- a/WhackerLinkConsoleV2/SettingsManager.cs
+++ b/DVMConsole/SettingsManager.cs
@@ -1,116 +1,109 @@
-/*
-* WhackerLink - WhackerLinkConsoleV2
-*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, either version 3 of the License, or
-* (at your option) any later version.
-*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
-*
-* You should have received a copy of the GNU General Public License
-* along with this program. If not, see .
-*
-* Copyright (C) 2024-2025 Caleb, K4PHP
-*
-*/
-
-using System.IO;
-using Newtonsoft.Json;
-
-namespace WhackerLinkConsoleV2
-{
- public class SettingsManager
- {
- private const string SettingsFilePath = "UserSettings.json";
-
- public bool ShowSystemStatus { get; set; } = true;
- public bool ShowChannels { get; set; } = true;
- public bool ShowAlertTones { get; set; } = true;
-
- public string LastCodeplugPath { get; set; } = null;
-
- public Dictionary ChannelPositions { get; set; } = new Dictionary();
- public Dictionary SystemStatusPositions { get; set; } = new Dictionary();
- public List AlertToneFilePaths { get; set; } = new List();
- public Dictionary AlertTonePositions { get; set; } = new Dictionary();
- public Dictionary ChannelOutputDevices { get; set; } = new Dictionary();
-
- public void LoadSettings()
- {
- if (!File.Exists(SettingsFilePath)) return;
-
- try
- {
- var json = File.ReadAllText(SettingsFilePath);
- var loadedSettings = JsonConvert.DeserializeObject(json);
-
- if (loadedSettings != null)
- {
- ShowSystemStatus = loadedSettings.ShowSystemStatus;
- ShowChannels = loadedSettings.ShowChannels;
- ShowAlertTones = loadedSettings.ShowAlertTones;
- LastCodeplugPath = loadedSettings.LastCodeplugPath;
- ChannelPositions = loadedSettings.ChannelPositions ?? new Dictionary();
- SystemStatusPositions = loadedSettings.SystemStatusPositions ?? new Dictionary();
- AlertToneFilePaths = loadedSettings.AlertToneFilePaths ?? new List();
- AlertTonePositions = loadedSettings.AlertTonePositions ?? new Dictionary();
- ChannelOutputDevices = loadedSettings.ChannelOutputDevices ?? new Dictionary();
- }
- }
- catch (Exception ex)
- {
- Console.WriteLine($"Error loading settings: {ex.Message}");
- }
- }
-
- public void UpdateAlertTonePaths(string newFilePath)
- {
- if (!AlertToneFilePaths.Contains(newFilePath))
- {
- AlertToneFilePaths.Add(newFilePath);
- SaveSettings();
- }
- }
-
- public void UpdateAlertTonePosition(string alertFileName, double x, double y)
- {
- AlertTonePositions[alertFileName] = new ChannelPosition { X = x, Y = y };
- SaveSettings();
- }
-
- public void UpdateChannelPosition(string channelName, double x, double y)
- {
- ChannelPositions[channelName] = new ChannelPosition { X = x, Y = y };
- SaveSettings();
- }
-
- public void UpdateSystemStatusPosition(string systemName, double x, double y)
- {
- SystemStatusPositions[systemName] = new ChannelPosition { X = x, Y = y };
- SaveSettings();
- }
-
- public void UpdateChannelOutputDevice(string channelName, int deviceIndex)
- {
- ChannelOutputDevices[channelName] = deviceIndex;
- SaveSettings();
- }
-
- public void SaveSettings()
- {
- try
- {
- var json = JsonConvert.SerializeObject(this, Formatting.Indented);
- File.WriteAllText(SettingsFilePath, json);
- }
- catch (Exception ex)
- {
- Console.WriteLine($"Error saving settings: {ex.Message}");
- }
- }
- }
-}
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - DVMConsole
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / DVM Console
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
+*
+* Copyright (C) 2024-2025 Caleb, K4PHP
+*
+*/
+
+using System.IO;
+using Newtonsoft.Json;
+
+namespace DVMConsole
+{
+ public class SettingsManager
+ {
+ private const string SettingsFilePath = "UserSettings.json";
+
+ public bool ShowSystemStatus { get; set; } = true;
+ public bool ShowChannels { get; set; } = true;
+ public bool ShowAlertTones { get; set; } = true;
+
+ public string LastCodeplugPath { get; set; } = null;
+
+ public Dictionary ChannelPositions { get; set; } = new Dictionary();
+ public Dictionary SystemStatusPositions { get; set; } = new Dictionary();
+ public List AlertToneFilePaths { get; set; } = new List();
+ public Dictionary AlertTonePositions { get; set; } = new Dictionary();
+ public Dictionary ChannelOutputDevices { get; set; } = new Dictionary();
+
+ public void LoadSettings()
+ {
+ if (!File.Exists(SettingsFilePath)) return;
+
+ try
+ {
+ var json = File.ReadAllText(SettingsFilePath);
+ var loadedSettings = JsonConvert.DeserializeObject(json);
+
+ if (loadedSettings != null)
+ {
+ ShowSystemStatus = loadedSettings.ShowSystemStatus;
+ ShowChannels = loadedSettings.ShowChannels;
+ ShowAlertTones = loadedSettings.ShowAlertTones;
+ LastCodeplugPath = loadedSettings.LastCodeplugPath;
+ ChannelPositions = loadedSettings.ChannelPositions ?? new Dictionary();
+ SystemStatusPositions = loadedSettings.SystemStatusPositions ?? new Dictionary();
+ AlertToneFilePaths = loadedSettings.AlertToneFilePaths ?? new List();
+ AlertTonePositions = loadedSettings.AlertTonePositions ?? new Dictionary();
+ ChannelOutputDevices = loadedSettings.ChannelOutputDevices ?? new Dictionary();
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error loading settings: {ex.Message}");
+ }
+ }
+
+ public void UpdateAlertTonePaths(string newFilePath)
+ {
+ if (!AlertToneFilePaths.Contains(newFilePath))
+ {
+ AlertToneFilePaths.Add(newFilePath);
+ SaveSettings();
+ }
+ }
+
+ public void UpdateAlertTonePosition(string alertFileName, double x, double y)
+ {
+ AlertTonePositions[alertFileName] = new ChannelPosition { X = x, Y = y };
+ SaveSettings();
+ }
+
+ public void UpdateChannelPosition(string channelName, double x, double y)
+ {
+ ChannelPositions[channelName] = new ChannelPosition { X = x, Y = y };
+ SaveSettings();
+ }
+
+ public void UpdateSystemStatusPosition(string systemName, double x, double y)
+ {
+ SystemStatusPositions[systemName] = new ChannelPosition { X = x, Y = y };
+ SaveSettings();
+ }
+
+ public void UpdateChannelOutputDevice(string channelName, int deviceIndex)
+ {
+ ChannelOutputDevices[channelName] = deviceIndex;
+ SaveSettings();
+ }
+
+ public void SaveSettings()
+ {
+ try
+ {
+ var json = JsonConvert.SerializeObject(this, Formatting.Indented);
+ File.WriteAllText(SettingsFilePath, json);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error saving settings: {ex.Message}");
+ }
+ }
+ }
+}
diff --git a/WhackerLinkConsoleV2/SystemStatusBox.xaml b/DVMConsole/SystemStatusBox.xaml
similarity index 87%
rename from WhackerLinkConsoleV2/SystemStatusBox.xaml
rename to DVMConsole/SystemStatusBox.xaml
index ea5566d..1205df9 100644
--- a/WhackerLinkConsoleV2/SystemStatusBox.xaml
+++ b/DVMConsole/SystemStatusBox.xaml
@@ -1,11 +1,11 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/WhackerLinkConsoleV2/SystemStatusBox.xaml.cs b/DVMConsole/SystemStatusBox.xaml.cs
similarity index 59%
rename from WhackerLinkConsoleV2/SystemStatusBox.xaml.cs
rename to DVMConsole/SystemStatusBox.xaml.cs
index 85b59d3..f4beae1 100644
--- a/WhackerLinkConsoleV2/SystemStatusBox.xaml.cs
+++ b/DVMConsole/SystemStatusBox.xaml.cs
@@ -1,66 +1,59 @@
-/*
-* WhackerLink - WhackerLinkConsoleV2
-*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, either version 3 of the License, or
-* (at your option) any later version.
-*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
-*
-* You should have received a copy of the GNU General Public License
-* along with this program. If not, see .
-*
-* Copyright (C) 2024 Caleb, K4PHP
-*
-*/
-using System.ComponentModel;
-using System.Runtime.CompilerServices;
-using System.Windows.Controls;
-using WhackerLinkLib.Models;
-
-namespace WhackerLinkConsoleV2.Controls
-{
- public partial class SystemStatusBox : UserControl, INotifyPropertyChanged
- {
- private string _connectionState = "Disconnected";
-
- public string SystemName { get; set; }
- public string AddressPort { get; set; }
-
- public string ConnectionState
- {
- get => _connectionState;
- set
- {
- if (_connectionState != value)
- {
- _connectionState = value;
- NotifyPropertyChanged();
- }
- }
- }
-
- public SystemStatusBox()
- {
- InitializeComponent();
- DataContext = this;
- }
-
- public SystemStatusBox(string systemName, string address, int port) : this()
- {
- SystemName = systemName;
- AddressPort = $"Address: {address}:{port}";
- }
-
- public event PropertyChangedEventHandler PropertyChanged;
-
- protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
- }
-}
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - DVMConsole
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / DVM Console
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
+*
+* Copyright (C) 2025 Caleb, K4PHP
+*
+*/
+
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Windows.Controls;
+
+namespace DVMConsole.Controls
+{
+ public partial class SystemStatusBox : UserControl, INotifyPropertyChanged
+ {
+ private string _connectionState = "Disconnected";
+
+ public string SystemName { get; set; }
+ public string AddressPort { get; set; }
+
+ public string ConnectionState
+ {
+ get => _connectionState;
+ set
+ {
+ if (_connectionState != value)
+ {
+ _connectionState = value;
+ NotifyPropertyChanged();
+ }
+ }
+ }
+
+ public SystemStatusBox()
+ {
+ InitializeComponent();
+ DataContext = this;
+ }
+
+ public SystemStatusBox(string systemName, string address, int port) : this()
+ {
+ SystemName = systemName;
+ AddressPort = $"Address: {address}:{port}";
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
diff --git a/WhackerLinkConsoleV2/ToneGenerator.cs b/DVMConsole/ToneGenerator.cs
similarity index 77%
rename from WhackerLinkConsoleV2/ToneGenerator.cs
rename to DVMConsole/ToneGenerator.cs
index 1f24136..8b49fbc 100644
--- a/WhackerLinkConsoleV2/ToneGenerator.cs
+++ b/DVMConsole/ToneGenerator.cs
@@ -1,26 +1,19 @@
-/*
-* WhackerLink - WhackerLinkConsoleV2
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - DVMConsole
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, either version 3 of the License, or
-* (at your option) any later version.
+* @package DVM / DVM Console
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
+* Copyright (C) 2024-2025 Caleb, K4PHP
*
-* You should have received a copy of the GNU General Public License
-* along with this program. If not, see .
-*
-* Copyright (C) 2024 Caleb, K4PHP
-*
*/
using NAudio.Wave;
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
///
///
diff --git a/WhackerLinkConsoleV2/VocoderInterop.cs b/DVMConsole/VocoderInterop.cs
similarity index 99%
rename from WhackerLinkConsoleV2/VocoderInterop.cs
rename to DVMConsole/VocoderInterop.cs
index c177849..57bd0f9 100644
--- a/WhackerLinkConsoleV2/VocoderInterop.cs
+++ b/DVMConsole/VocoderInterop.cs
@@ -1,4 +1,4 @@
-// From W3AXL console
+// From https://github.com/w3axl/rc2-dvm
using System;
using System.Collections.Generic;
@@ -8,12 +8,9 @@ using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using fnecore;
-using Serilog;
-using WhackerLinkLib;
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
-
public enum MBE_MODE
{
DMR_AMBE, //! DMR AMBE
diff --git a/DVMConsole/VocoderToneLookupTable.cs b/DVMConsole/VocoderToneLookupTable.cs
new file mode 100644
index 0000000..a1afcb3
--- /dev/null
+++ b/DVMConsole/VocoderToneLookupTable.cs
@@ -0,0 +1,88 @@
+namespace DVMConsole
+{
+ ///
+ /// From https://github.com/W3AXL/rc2-dvm/blob/main/rc2-dvm/Audio.cs
+ /// No license info or copyright header was present, but attempting to give credit where credit is due.
+ ///
+ public class VocoderToneLookupTable
+ {
+ ///
+ /// This lookup table was obtained by recording the single-tone frames output from a EF Johnson VP8000
+ ///
+ public static SortedDictionary IMBEToneFrames = new SortedDictionary()
+ {
+ { 281, new byte[] { 0x10, 0xFF, 0xF9, 0x45, 0x31, 0xCC, 0x8A, 0xC4, 0x3C, 0x16, 0x4B } },
+ { 313, new byte[] { 0x0C, 0xFF, 0xF9, 0x45, 0x31, 0xCC, 0x8A, 0xC4, 0x3C, 0x17, 0x48 } },
+ { 344, new byte[] { 0x5, 0x7F, 0xBC, 0xD6, 0xC6, 0x75, 0x80, 0x43, 0x39, 0x53, 0x9F } },
+ { 375, new byte[] { 0x1, 0x7F, 0xF3, 0x13, 0xF1, 0xD9, 0x8A, 0xAC, 0x1D, 0x8F, 0xC6 } },
+ { 406, new byte[] { 0x24, 0xF7, 0x7C, 0x1F, 0x40, 0x78, 0x81, 0xA0, 0x50, 0xBA, 0x0F } },
+ { 438, new byte[] { 0x20, 0xF6, 0x67, 0x81, 0xF2, 0x96, 0x82, 0xAE, 0xA5, 0x47, 0x5C } },
+ { 469, new byte[] { 0x1C, 0xFE, 0x54, 0xE9, 0x48, 0x4D, 0x86, 0x88, 0x54, 0x4F, 0x52 } },
+ { 500, new byte[] { 0x18, 0xDF, 0x94, 0x2A, 0x5F, 0x28, 0x86, 0x20, 0x0B, 0xF6, 0xF2 } },
+ { 531, new byte[] { 0x14, 0xF6, 0xE7, 0x2, 0xA4, 0xC5, 0x86, 0x24, 0x31, 0x58, 0x9A } },
+ { 563, new byte[] { 0x10, 0xDF, 0x60, 0x5C, 0x3D, 0x4B, 0x8C, 0x44, 0xEC, 0x20, 0x4A } },
+ { 594, new byte[] { 0x0C, 0xDF, 0x60, 0x5C, 0x3D, 0x4B, 0x2C, 0x44, 0xAC, 0x39, 0x6C } },
+ { 625, new byte[] { 0x0C, 0xDF, 0x60, 0x5C, 0x3D, 0x4B, 0x8C, 0x44, 0xEC, 0x61, 0x48 } },
+ { 656, new byte[] { 0x9, 0x76, 0x98, 0x2C, 0x66, 0xF1, 0x82, 0x18, 0x35, 0x2F, 0x2B } },
+ { 688, new byte[] { 0x5, 0x5C, 0x30, 0xE2, 0x82, 0x79, 0x88, 0x13, 0x9, 0x54, 0x9E } },
+ { 719, new byte[] { 0x5, 0x5C, 0x31, 0xE0, 0x80, 0x78, 0x88, 0x11, 0x28, 0x4C, 0x1A } },
+ { 750, new byte[] { 0x1, 0x61, 0xC2, 0x83, 0x1, 0xD9, 0x90, 0x2E, 0x98, 0x88, 0xCF } },
+ { 781, new byte[] { 0x1, 0x61, 0xC3, 0x82, 0x0, 0xD8, 0x90, 0xA4, 0x12, 0x0A, 0x4B } },
+ { 813, new byte[] { 0x15, 0x47, 0x9D, 0x1B, 0xDC, 0xED, 0x82, 0x20, 0x71, 0x1E, 0x98 } },
+ { 844, new byte[] { 0x11, 0x50, 0xD9, 0xF7, 0xA9, 0x4F, 0x89, 0xC7, 0x9C, 0x13, 0x43 } },
+ { 875, new byte[] { 0x0D, 0x50, 0xD3, 0xFD, 0xBC, 0x4D, 0xC5, 0xC4, 0x9C, 0x3, 0x4E } },
+ { 906, new byte[] { 0x0D, 0x50, 0xD3, 0xFD, 0xBC, 0x4D, 0xC5, 0xC4, 0x1C, 0x13, 0x4A } },
+ { 938, new byte[] { 0x0D, 0x50, 0xD3, 0xFD, 0xB4, 0x4F, 0xC5, 0xC5, 0x1C, 0x73, 0x41 } },
+ { 969, new byte[] { 0x9, 0x23, 0x0B, 0x0D, 0xC4, 0xA5, 0xC8, 0xE8, 0xA8, 0x2A, 0x2D } },
+ { 1000, new byte[] { 0x9, 0x23, 0x0B, 0x0D, 0xC4, 0xA5, 0xCA, 0xE8, 0x28, 0x0A, 0x32 } },
+ { 1031, new byte[] { 0x5, 0x51, 0xFB, 0xCA, 0xCE, 0x49, 0xCC, 0x3, 0x25, 0x59, 0x97 } },
+ { 1063, new byte[] { 0x5, 0x51, 0xFB, 0xCA, 0xCE, 0x49, 0xCC, 0x3, 0x25, 0x59, 0x94 } },
+ { 1094, new byte[] { 0x5, 0x51, 0xFB, 0xCA, 0xCE, 0x49, 0x0C, 0x43, 0x5, 0x41, 0x90 } },
+ { 1125, new byte[] { 0x1, 0xC7, 0x9B, 0x13, 0x31, 0x59, 0x80, 0xAA, 0x99, 0x89, 0xCF } },
+ { 1156, new byte[] { 0x1, 0xE7, 0x1A, 0x12, 0x30, 0x58, 0x80, 0xA2, 0x91, 0x89, 0xCD } },
+ { 1188, new byte[] { 0x1, 0xE7, 0x1A, 0x12, 0x30, 0x58, 0x80, 0xA2, 0x91, 0x89, 0xCA } },
+ { 1219, new byte[] { 0x0D, 0x50, 0x93, 0x4D, 0x28, 0x0D, 0x4D, 0x85, 0xB4, 0x32, 0x43 } },
+ { 1250, new byte[] { 0x0D, 0x50, 0x93, 0x4D, 0x28, 0x0D, 0x4D, 0x85, 0xB4, 0x32, 0x41 } },
+ { 1281, new byte[] { 0x9, 0x24, 0x7D, 0x7B, 0xD4, 0xDD, 0x49, 0xB8, 0xB6, 0x2C, 0xA5 } },
+ { 1313, new byte[] { 0x9, 0x24, 0x7D, 0x7B, 0xD4, 0xDD, 0x45, 0xB8, 0xB6, 0x2C, 0xAA } },
+ { 1344, new byte[] { 0x9, 0x24, 0x7D, 0x7B, 0xD4, 0xDD, 0x45, 0xF8, 0xA6, 0x2A, 0xB8 } },
+ { 1375, new byte[] { 0x5, 0xC1, 0x8A, 0xE8, 0x94, 0x2C, 0x41, 0x1D, 0x22, 0x45, 0x16 } },
+ { 1406, new byte[] { 0x5, 0xC1, 0x8A, 0xE8, 0x98, 0x66, 0x40, 0x7D, 0x32, 0x5D, 0x14 } },
+ { 1438, new byte[] { 0x5, 0xC1, 0x8A, 0xE8, 0x98, 0x66, 0x40, 0x7D, 0x32, 0x5D, 0x13 } },
+ { 1469, new byte[] { 0x5, 0xC1, 0x8A, 0xE8, 0x98, 0x66, 0x40, 0x7D, 0x32, 0x57, 0x11 } },
+ { 1500, new byte[] { 0x1, 0x2D, 0xA7, 0x2A, 0xDD, 0xA8, 0x5C, 0xC8, 0x5C, 0x49, 0x46 } },
+ { 1531, new byte[] { 0x1, 0x2D, 0xA7, 0x2A, 0xDD, 0xA8, 0x5C, 0xC8, 0x5C, 0x49, 0x65 } },
+ { 1563, new byte[] { 0x1, 0x2D, 0xA7, 0x2A, 0xDD, 0xA8, 0x5C, 0xC8, 0x58, 0x4F, 0x62 } },
+ { 1594, new byte[] { 0x1, 0x2D, 0xA7, 0x2A, 0xDD, 0xA8, 0x5C, 0x48, 0xDC, 0xCB, 0xE3 } },
+ { 1625, new byte[] { 0x9, 0x24, 0xF0, 0x6C, 0xC5, 0x55, 0x4B, 0xF0, 0x54, 0x30, 0x35 } },
+ { 1656, new byte[] { 0x9, 0x24, 0xF0, 0x6C, 0xC5, 0x55, 0x4B, 0xF0, 0x44, 0x34, 0x23 } },
+ { 1688, new byte[] { 0x9, 0x24, 0xF0, 0x6C, 0xC5, 0x55, 0x4B, 0xF0, 0x44, 0x24, 0x31 } },
+ { 1719, new byte[] { 0x5, 0x0B, 0x69, 0xE9, 0x8D, 0x74, 0x4A, 0x0D, 0x0A, 0xC5, 0x5E } },
+ { 1750, new byte[] { 0x5, 0x0B, 0x68, 0xEB, 0x8F, 0x65, 0x4A, 0x2F, 0x1B, 0xCD, 0xDD } },
+ { 1781, new byte[] { 0x5, 0x0B, 0x68, 0xEB, 0x8F, 0x65, 0x4A, 0x2F, 0x1B, 0xCD, 0xDA } },
+ { 1813, new byte[] { 0x5, 0x0B, 0x68, 0xEB, 0x8F, 0x65, 0x4A, 0x2F, 0x1B, 0xCD, 0xDB } },
+ { 1844, new byte[] { 0x5, 0x0B, 0x68, 0xEB, 0x8F, 0x65, 0x4A, 0x3F, 0x13, 0xC1, 0x58 } },
+ { 1875, new byte[] { 0x1, 0x2C, 0xA2, 0xA2, 0x55, 0x1, 0x5B, 0x0C, 0x92, 0x8D, 0xA7 } },
+ { 1906, new byte[] { 0x1, 0x2C, 0xA2, 0xA2, 0x55, 0x1, 0x53, 0x0C, 0x92, 0x8F, 0xAC } },
+ { 1938, new byte[] { 0x1, 0x2C, 0xA2, 0xA2, 0x55, 0x1, 0x53, 0x0C, 0x92, 0x8B, 0x2D } },
+ { 1969, new byte[] { 0x1, 0x2C, 0xA2, 0xA2, 0x55, 0x1, 0x53, 0x0C, 0x92, 0x8A, 0x2B } },
+ { 2000, new byte[] { 0x1, 0x2C, 0xA2, 0xA2, 0x55, 0x1, 0x53, 0x0C, 0x92, 0x83, 0x2A } },
+ { 2031, new byte[] { 0x9, 0x0E, 0xD4, 0x38, 0xDF, 0x3D, 0x6C, 0xD4, 0x2F, 0x0E, 0xE8 } },
+ { 2063, new byte[] { 0x5, 0x0B, 0x59, 0x4C, 0xC0, 0x48, 0x6C, 0x1C, 0x4E, 0x5C, 0x0E } },
+ { 2094, new byte[] { 0x5, 0x0B, 0x58, 0x4E, 0xC2, 0x49, 0x6C, 0x1E, 0x4F, 0x5C, 0x8C } },
+ { 2125, new byte[] { 0x5, 0x0B, 0x58, 0x4E, 0xD2, 0x41, 0x6C, 0x0E, 0x47, 0x58, 0x8D } },
+ { 2156, new byte[] { 0x5, 0x0B, 0x58, 0x4E, 0xD2, 0x41, 0x6C, 0x0E, 0x47, 0x50, 0x82 } },
+ { 2188, new byte[] { 0x5, 0x0B, 0x58, 0x4E, 0xD2, 0x41, 0x6C, 0x0E, 0x47, 0x40, 0x0 } },
+ { 2219, new byte[] { 0x5, 0x0B, 0x58, 0x4E, 0xD2, 0x41, 0x6C, 0x0E, 0x47, 0x40, 0x1 } },
+ { 2250, new byte[] { 0x1, 0xAA, 0x66, 0xBF, 0x5C, 0x4, 0x42, 0x60, 0xFA, 0x6D, 0xAE } },
+ { 2281, new byte[] { 0x1, 0xAA, 0x66, 0xBF, 0x5C, 0x4, 0x42, 0x60, 0xFB, 0x6D, 0xAE } },
+ { 2313, new byte[] { 0x1, 0xAA, 0x66, 0xBF, 0x5C, 0x4, 0x42, 0x60, 0xFA, 0x63, 0x2D } },
+ { 2344, new byte[] { 0x1, 0xAA, 0x66, 0xBF, 0x5C, 0x4, 0x42, 0x62, 0xF8, 0x61, 0x2B } },
+ { 2375, new byte[] { 0x1, 0xAA, 0x66, 0xBF, 0x5C, 0x4, 0x42, 0x62, 0xF9, 0x60, 0x2A } },
+ { 2406, new byte[] { 0x5, 0x6, 0xFB, 0x63, 0xCD, 0xD9, 0x2B, 0x42, 0xE1, 0xD7, 0x6F } },
+ { 2438, new byte[] { 0x5, 0x6, 0xFB, 0x63, 0xCD, 0xD9, 0x2B, 0x42, 0xE1, 0xC7, 0x6C } },
+ { 2469, new byte[] { 0x5, 0x6, 0xFB, 0x63, 0xCD, 0xD9, 0x2B, 0x42, 0xE1, 0xCF, 0x6D } },
+ { 2500, new byte[] { 0x5, 0x6, 0xFB, 0x63, 0xCD, 0xD9, 0x2B, 0x42, 0xE1, 0xCF, 0x6B } },
+ };
+ }
+}
diff --git a/WhackerLinkConsoleV2/WaveFilePlaybackManager.cs b/DVMConsole/WaveFilePlaybackManager.cs
similarity index 73%
rename from WhackerLinkConsoleV2/WaveFilePlaybackManager.cs
rename to DVMConsole/WaveFilePlaybackManager.cs
index 10fe26b..ff11299 100644
--- a/WhackerLinkConsoleV2/WaveFilePlaybackManager.cs
+++ b/DVMConsole/WaveFilePlaybackManager.cs
@@ -1,27 +1,20 @@
-/*
-* WhackerLink - WhackerLinkConsoleV2
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - DVMConsole
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, either version 3 of the License, or
-* (at your option) any later version.
+* @package DVM / DVM Console
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
+* Copyright (C) 2024 Caleb, K4PHP
*
-* You should have received a copy of the GNU General Public License
-* along with this program. If not, see .
-*
-* Copyright (C) 2024 Caleb, K4PHP
-*
*/
using NAudio.Wave;
using System.Windows.Threading;
-namespace WhackerLinkConsoleV2
+namespace DVMConsole
{
public class WaveFilePlaybackManager
{
diff --git a/WhackerLinkConsoleV2/WidgetSelectionWindow.xaml b/DVMConsole/WidgetSelectionWindow.xaml
similarity index 90%
rename from WhackerLinkConsoleV2/WidgetSelectionWindow.xaml
rename to DVMConsole/WidgetSelectionWindow.xaml
index d2cd7e3..1186f7f 100644
--- a/WhackerLinkConsoleV2/WidgetSelectionWindow.xaml
+++ b/DVMConsole/WidgetSelectionWindow.xaml
@@ -1,12 +1,12 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/DVMConsole/WidgetSelectionWindow.xaml.cs b/DVMConsole/WidgetSelectionWindow.xaml.cs
new file mode 100644
index 0000000..4b160f0
--- /dev/null
+++ b/DVMConsole/WidgetSelectionWindow.xaml.cs
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+/**
+* Digital Voice Modem - DVMConsole
+* AGPLv3 Open Source. Use is subject to license terms.
+* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+*
+* @package DVM / DVM Console
+* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
+*
+* Copyright (C) 2024 Caleb, K4PHP
+*
+*/
+
+using System.Windows;
+
+namespace DVMConsole
+{
+ public partial class WidgetSelectionWindow : Window
+ {
+ public bool ShowSystemStatus { get; private set; } = true;
+ public bool ShowChannels { get; private set; } = true;
+ public bool ShowAlertTones { get; private set; } = true;
+
+ public WidgetSelectionWindow()
+ {
+ InitializeComponent();
+ }
+
+ private void ApplyButton_Click(object sender, RoutedEventArgs e)
+ {
+ ShowSystemStatus = SystemStatusCheckBox.IsChecked ?? false;
+ ShowChannels = ChannelCheckBox.IsChecked ?? false;
+ ShowAlertTones = AlertToneCheckBox.IsChecked ?? false;
+ DialogResult = true;
+ Close();
+ }
+ }
+}
diff --git a/DVMConsole/codeplugs/codeplug.yml b/DVMConsole/codeplugs/codeplug.yml
new file mode 100644
index 0000000..09cb896
--- /dev/null
+++ b/DVMConsole/codeplugs/codeplug.yml
@@ -0,0 +1,35 @@
+systems:
+ - name: "System 1"
+ address: "localhost"
+ port: 3001
+ authKey: "RPT_PASSWORD"
+ peerId: 1234567
+ rid: "12345"
+
+zones:
+ - name: "Zone 1"
+ channels:
+ - name: "Channel 1"
+ system: "System 1"
+ tgid: "2001"
+ keyId: 0x50
+ algoId: 0xaa # 0xAA or 0x84 (RC4 or AES)
+ encryptionKey: null # Ignored now, we use dvmfne KMM support
+ - name: "Channel 2"
+ system: "System 1"
+ tgid: "15002"
+ - name: "Channel 3"
+ system: "System 1"
+ tgid: "15003"
+
+ - name: "Zone 2"
+ channels:
+ - name: "Channel A"
+ system: "System 1"
+ tgid: "16001"
+ - name: "Channel B"
+ system: "System 1"
+ tgid: "16002"
+ - name: "Channel C"
+ system: "System 1"
+ tgid: "16002"
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index e72bfdd..bae94e1 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,5 +1,5 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
@@ -7,17 +7,15 @@
Preamble
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
+our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
+software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
The precise terms and conditions for copying, distribution and
modification follow.
@@ -72,7 +60,7 @@ modification follow.
0. Definitions.
- "This License" refers to version 3 of the GNU General Public License.
+ "This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
- 13. Use with the GNU Affero General Public License.
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
+under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
+Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
+GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
+versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
+ it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
+ GNU Affero General Public License for more details.
- You should have received a copy of the GNU General Public License
+ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- Copyright (C)
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-.
\ No newline at end of file
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
\ No newline at end of file
diff --git a/WhackerLinkConsoleV2/Assets/WhackerLinkLogoV2.png b/WhackerLinkConsoleV2/Assets/WhackerLinkLogoV2.png
deleted file mode 100644
index 4b9172d..0000000
Binary files a/WhackerLinkConsoleV2/Assets/WhackerLinkLogoV2.png and /dev/null differ
diff --git a/WhackerLinkConsoleV2/Assets/whackerlink-logo.png b/WhackerLinkConsoleV2/Assets/whackerlink-logo.png
deleted file mode 100644
index c4b0318..0000000
Binary files a/WhackerLinkConsoleV2/Assets/whackerlink-logo.png and /dev/null differ
diff --git a/WhackerLinkConsoleV2/ChannelPosition.cs b/WhackerLinkConsoleV2/ChannelPosition.cs
deleted file mode 100644
index 1af6ab0..0000000
--- a/WhackerLinkConsoleV2/ChannelPosition.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
-* WhackerLink - WhackerLinkConsoleV2
-*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, either version 3 of the License, or
-* (at your option) any later version.
-*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
-*
-* You should have received a copy of the GNU General Public License
-* along with this program. If not, see .
-*
-* Copyright (C) 2024 Caleb, K4PHP
-*
-*/
-
-namespace WhackerLinkConsoleV2
-{
- public class ChannelPosition
- {
- public double X { get; set; }
- public double Y { get; set; }
- }
-}
diff --git a/WhackerLinkConsoleV2/ConsoleNative.cs b/WhackerLinkConsoleV2/ConsoleNative.cs
deleted file mode 100644
index 03e7236..0000000
--- a/WhackerLinkConsoleV2/ConsoleNative.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
-* WhackerLink - WhackerLinkConsoleV2
-*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, either version 3 of the License, or
-* (at your option) any later version.
-*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
-*
-* You should have received a copy of the GNU General Public License
-* along with this program. If not, see .
-*
-* Copyright (C) 2025 Caleb, K4PHP
-*
-*/
-
-using System.Runtime.InteropServices;
-
-namespace WhackerLinkConsoleV2
-{
- public static class ConsoleNative
- {
- [DllImport("kernel32.dll")]
- private static extern bool AllocConsole();
-
- public static void ShowConsole()
- {
- AllocConsole();
- Console.WriteLine("Console attached.");
- }
- }
-}
diff --git a/WhackerLinkConsoleV2/WidgetSelectionWindow.xaml.cs b/WhackerLinkConsoleV2/WidgetSelectionWindow.xaml.cs
deleted file mode 100644
index d837eb0..0000000
--- a/WhackerLinkConsoleV2/WidgetSelectionWindow.xaml.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
-* WhackerLink - WhackerLinkConsoleV2
-*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU General Public License as published by
-* the Free Software Foundation, either version 3 of the License, or
-* (at your option) any later version.
-*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU General Public License for more details.
-*
-* You should have received a copy of the GNU General Public License
-* along with this program. If not, see .
-*
-* Copyright (C) 2024 Caleb, K4PHP
-*
-*/
-
-using System.Windows;
-
-namespace WhackerLinkConsoleV2
-{
- public partial class WidgetSelectionWindow : Window
- {
- public bool ShowSystemStatus { get; private set; } = true;
- public bool ShowChannels { get; private set; } = true;
- public bool ShowAlertTones { get; private set; } = true;
-
- public WidgetSelectionWindow()
- {
- InitializeComponent();
- }
-
- private void ApplyButton_Click(object sender, RoutedEventArgs e)
- {
- ShowSystemStatus = SystemStatusCheckBox.IsChecked ?? false;
- ShowChannels = ChannelCheckBox.IsChecked ?? false;
- ShowAlertTones = AlertToneCheckBox.IsChecked ?? false;
- DialogResult = true;
- Close();
- }
- }
-}
diff --git a/WhackerLinkConsoleV2/codeplugs/codeplug.yml b/WhackerLinkConsoleV2/codeplugs/codeplug.yml
deleted file mode 100644
index ca0f856..0000000
--- a/WhackerLinkConsoleV2/codeplugs/codeplug.yml
+++ /dev/null
@@ -1,74 +0,0 @@
-radioWide:
- hostVersion: "R01.00.00"
- codeplugVersion: "R01.00.00"
- radioAlias: ""
- serialNumber: "123ABC1234"
- model: "CONSOLE"
- inCarMode: "N/A"
-
-systems:
- - name: "System 1"
- address: "localhost"
- port: 3000
- authKey: ""
- rid: "12345"
- site:
- name: "Central Site"
- controlChannel: "772.74375"
- voiceChannels: []
- location:
- x: 757.89
- y: 1274.17
- z: 360.3
- systemID: 1
- siteID: 1
- range: 1.5
-
- # Example DVM Project FNE system
- - name: "System 2"
- address: "localhost"
- port: 3001
- authKey: "RPT_PASSWORD"
- peerId: 1234567
- isDvm: true
- # Note: for proper operation with affiliated traffic, this id will use +1 for each channel you have for affiliation purposes
- rid: "12345"
- site:
- name: "Central Site"
- controlChannel: "772.74375"
- voiceChannels: []
- location:
- x: 757.89
- y: 1274.17
- z: 360.3
- systemID: 1
- siteID: 1
- range: 1.5
-
-zones:
- - name: "Zone 1"
- channels:
- - name: "Channel 1"
- system: "System 1"
- tgid: "2001"
- keyId: 0x50
- algoId: 0xaa # 0xAA or 0x84 (RC4 or AES)
- encryptionKey: null # Ignored now, we use dvmfne KMM support
- - name: "Channel 2"
- system: "System 1"
- tgid: "15002"
- - name: "Channel 3"
- system: "System 1"
- tgid: "15003"
-
- - name: "Zone 2"
- channels:
- - name: "Channel A"
- system: "System 1"
- tgid: "16001"
- - name: "Channel B"
- system: "System 1"
- tgid: "16002"
- - name: "Channel C"
- system: "System 1"
- tgid: "16002"
\ No newline at end of file
diff --git a/WhackerLinkLib b/WhackerLinkLib
deleted file mode 160000
index 34ae0da..0000000
--- a/WhackerLinkLib
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 34ae0da1f296c1023455db9edc9b854f9b1eb0f2