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