From d69e74237ab754df2bd6c8f5fbd73047a9089d86 Mon Sep 17 00:00:00 2001 From: firealarmss Date: Wed, 19 Mar 2025 16:21:03 -0500 Subject: [PATCH] remove whackerlinklib submodule --- .gitmodules | 3 - WhackerLinkConsoleV2.sln => DVMConsole.sln | 198 +- .../AlertTone.xaml | 2 +- .../AlertTone.xaml.cs | 4 +- .../AliasTools.cs | 32 +- .../AmbeNative.cs | 20 +- {WhackerLinkConsoleV2 => DVMConsole}/App.xaml | 170 +- .../App.xaml.cs | 28 +- .../AssemblyInfo.cs | 20 +- DVMConsole/Assets/DvmLogo.png | Bin 0 -> 115672 bytes .../Assets/WhackerLinkLogoV4.png | Bin .../Assets/alerttone.png | Bin .../Assets/alerttone2.png | Bin .../Assets/audio.png | Bin .../Assets/channelmarker.png | Bin .../Assets/clearemerg.png | Bin .../Assets/config.png | Bin .../Assets/config2.png | Bin .../Assets/connection.png | Bin .../Assets/instantptt.png | Bin .../Assets/page.png | Bin .../Assets/pageselect.png | Bin .../Assets/pttselect.png | Bin .../Audio/alert1.wav | Bin .../Audio/alert2.wav | Bin .../Audio/alert3.wav | Bin .../Audio/emergency.wav | Bin .../Audio/hold.wav | Bin DVMConsole/AudioConverter.cs | 92 + .../AudioManager.cs | 28 +- .../AudioSettingsWindow.xaml | 2 +- .../AudioSettingsWindow.xaml.cs | 26 +- .../CallHistoryWindow.xaml | 4 +- .../CallHistoryWindow.xaml.cs | 18 +- .../ChannelBox.xaml | 228 +- .../ChannelBox.xaml.cs | 673 ++- DVMConsole/ChannelPosition.cs | 21 + DVMConsole/Codeplug.cs | 136 + DVMConsole/ConsoleNative.cs | 29 + .../DVMConsole.csproj | 233 +- .../DigitalPageWindow.xaml | 2 +- .../DigitalPageWindow.xaml.cs | 26 +- .../FlashingBackgroundManager.cs | 25 +- .../FneSystemBase.DMR.cs | 23 +- .../FneSystemBase.NXDN.cs | 20 +- .../FneSystemBase.P25.cs | 26 +- .../FneSystemBase.cs | 27 +- .../FneSystemManager.cs | 28 +- .../GainSampleProvider.cs | 25 +- .../KeyStatusWindow.xaml | 2 +- .../KeyStatusWindow.xaml.cs | 22 +- .../MBEToneDetector.cs | 2 +- .../MainWindow.xaml | 440 +- .../MainWindow.xaml.cs | 3880 +++++++---------- .../P25Crypto.cs | 30 +- .../PeerSystem.cs | 30 +- .../QuickCallPage.xaml | 4 +- .../QuickCallPage.xaml.cs | 4 +- .../SampleTimeConvert.cs | 3 +- .../SelectedChannelsManager.cs | 27 +- .../SettingsManager.cs | 225 +- .../SystemStatusBox.xaml | 20 +- .../SystemStatusBox.xaml.cs | 125 +- .../ToneGenerator.cs | 25 +- .../VocoderInterop.cs | 7 +- DVMConsole/VocoderToneLookupTable.cs | 88 + .../WaveFilePlaybackManager.cs | 25 +- .../WidgetSelectionWindow.xaml | 24 +- DVMConsole/WidgetSelectionWindow.xaml.cs | 38 + DVMConsole/codeplugs/codeplug.yml | 35 + LICENSE | 149 +- .../Assets/WhackerLinkLogoV2.png | Bin 67210 -> 0 bytes .../Assets/whackerlink-logo.png | Bin 22009 -> 0 bytes WhackerLinkConsoleV2/ChannelPosition.cs | 28 - WhackerLinkConsoleV2/ConsoleNative.cs | 36 - .../WidgetSelectionWindow.xaml.cs | 45 - WhackerLinkConsoleV2/codeplugs/codeplug.yml | 74 - WhackerLinkLib | 1 - 78 files changed, 3537 insertions(+), 4021 deletions(-) rename WhackerLinkConsoleV2.sln => DVMConsole.sln (68%) rename {WhackerLinkConsoleV2 => DVMConsole}/AlertTone.xaml (96%) rename {WhackerLinkConsoleV2 => DVMConsole}/AlertTone.xaml.cs (97%) rename {WhackerLinkConsoleV2 => DVMConsole}/AliasTools.cs (58%) rename {WhackerLinkConsoleV2 => DVMConsole}/AmbeNative.cs (97%) rename {WhackerLinkConsoleV2 => DVMConsole}/App.xaml (98%) rename {WhackerLinkConsoleV2 => DVMConsole}/App.xaml.cs (82%) rename {WhackerLinkConsoleV2 => DVMConsole}/AssemblyInfo.cs (98%) create mode 100644 DVMConsole/Assets/DvmLogo.png rename {WhackerLinkConsoleV2 => DVMConsole}/Assets/WhackerLinkLogoV4.png (100%) rename {WhackerLinkConsoleV2 => DVMConsole}/Assets/alerttone.png (100%) rename {WhackerLinkConsoleV2 => DVMConsole}/Assets/alerttone2.png (100%) rename {WhackerLinkConsoleV2 => DVMConsole}/Assets/audio.png (100%) rename {WhackerLinkConsoleV2 => DVMConsole}/Assets/channelmarker.png (100%) rename {WhackerLinkConsoleV2 => DVMConsole}/Assets/clearemerg.png (100%) rename {WhackerLinkConsoleV2 => DVMConsole}/Assets/config.png (100%) rename {WhackerLinkConsoleV2 => DVMConsole}/Assets/config2.png (100%) rename {WhackerLinkConsoleV2 => DVMConsole}/Assets/connection.png (100%) rename {WhackerLinkConsoleV2 => DVMConsole}/Assets/instantptt.png (100%) rename {WhackerLinkConsoleV2 => DVMConsole}/Assets/page.png (100%) rename {WhackerLinkConsoleV2 => DVMConsole}/Assets/pageselect.png (100%) rename {WhackerLinkConsoleV2 => DVMConsole}/Assets/pttselect.png (100%) rename {WhackerLinkConsoleV2 => DVMConsole}/Audio/alert1.wav (100%) rename {WhackerLinkConsoleV2 => DVMConsole}/Audio/alert2.wav (100%) rename {WhackerLinkConsoleV2 => DVMConsole}/Audio/alert3.wav (100%) rename {WhackerLinkConsoleV2 => DVMConsole}/Audio/emergency.wav (100%) rename {WhackerLinkConsoleV2 => DVMConsole}/Audio/hold.wav (100%) create mode 100644 DVMConsole/AudioConverter.cs rename {WhackerLinkConsoleV2 => DVMConsole}/AudioManager.cs (82%) rename {WhackerLinkConsoleV2 => DVMConsole}/AudioSettingsWindow.xaml (95%) rename {WhackerLinkConsoleV2 => DVMConsole}/AudioSettingsWindow.xaml.cs (83%) rename {WhackerLinkConsoleV2 => DVMConsole}/CallHistoryWindow.xaml (90%) rename {WhackerLinkConsoleV2 => DVMConsole}/CallHistoryWindow.xaml.cs (87%) rename {WhackerLinkConsoleV2 => DVMConsole}/ChannelBox.xaml (92%) rename {WhackerLinkConsoleV2 => DVMConsole}/ChannelBox.xaml.cs (90%) create mode 100644 DVMConsole/ChannelPosition.cs create mode 100644 DVMConsole/Codeplug.cs create mode 100644 DVMConsole/ConsoleNative.cs rename WhackerLinkConsoleV2/WhackerLinkConsoleV2.csproj => DVMConsole/DVMConsole.csproj (57%) rename {WhackerLinkConsoleV2 => DVMConsole}/DigitalPageWindow.xaml (94%) rename {WhackerLinkConsoleV2 => DVMConsole}/DigitalPageWindow.xaml.cs (54%) rename {WhackerLinkConsoleV2 => DVMConsole}/FlashingBackgroundManager.cs (82%) rename {WhackerLinkConsoleV2 => DVMConsole}/FneSystemBase.DMR.cs (90%) rename {WhackerLinkConsoleV2 => DVMConsole}/FneSystemBase.NXDN.cs (80%) rename {WhackerLinkConsoleV2 => DVMConsole}/FneSystemBase.P25.cs (97%) rename {WhackerLinkConsoleV2 => DVMConsole}/FneSystemBase.cs (93%) rename {WhackerLinkConsoleV2 => DVMConsole}/FneSystemManager.cs (75%) rename {WhackerLinkConsoleV2 => DVMConsole}/GainSampleProvider.cs (55%) rename {WhackerLinkConsoleV2 => DVMConsole}/KeyStatusWindow.xaml (94%) rename {WhackerLinkConsoleV2 => DVMConsole}/KeyStatusWindow.xaml.cs (87%) rename {WhackerLinkConsoleV2 => DVMConsole}/MBEToneDetector.cs (99%) rename {WhackerLinkConsoleV2 => DVMConsole}/MainWindow.xaml (94%) rename {WhackerLinkConsoleV2 => DVMConsole}/MainWindow.xaml.cs (64%) rename {WhackerLinkConsoleV2 => DVMConsole}/P25Crypto.cs (92%) rename {WhackerLinkConsoleV2 => DVMConsole}/PeerSystem.cs (78%) rename {WhackerLinkConsoleV2 => DVMConsole}/QuickCallPage.xaml (90%) rename {WhackerLinkConsoleV2 => DVMConsole}/QuickCallPage.xaml.cs (94%) rename {WhackerLinkConsoleV2 => DVMConsole}/SampleTimeConvert.cs (97%) rename {WhackerLinkConsoleV2 => DVMConsole}/SelectedChannelsManager.cs (60%) rename {WhackerLinkConsoleV2 => DVMConsole}/SettingsManager.cs (81%) rename {WhackerLinkConsoleV2 => DVMConsole}/SystemStatusBox.xaml (87%) rename {WhackerLinkConsoleV2 => DVMConsole}/SystemStatusBox.xaml.cs (59%) rename {WhackerLinkConsoleV2 => DVMConsole}/ToneGenerator.cs (77%) rename {WhackerLinkConsoleV2 => DVMConsole}/VocoderInterop.cs (99%) create mode 100644 DVMConsole/VocoderToneLookupTable.cs rename {WhackerLinkConsoleV2 => DVMConsole}/WaveFilePlaybackManager.cs (73%) rename {WhackerLinkConsoleV2 => DVMConsole}/WidgetSelectionWindow.xaml (90%) create mode 100644 DVMConsole/WidgetSelectionWindow.xaml.cs create mode 100644 DVMConsole/codeplugs/codeplug.yml delete mode 100644 WhackerLinkConsoleV2/Assets/WhackerLinkLogoV2.png delete mode 100644 WhackerLinkConsoleV2/Assets/whackerlink-logo.png delete mode 100644 WhackerLinkConsoleV2/ChannelPosition.cs delete mode 100644 WhackerLinkConsoleV2/ConsoleNative.cs delete mode 100644 WhackerLinkConsoleV2/WidgetSelectionWindow.xaml.cs delete mode 100644 WhackerLinkConsoleV2/codeplugs/codeplug.yml delete mode 160000 WhackerLinkLib 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 0000000000000000000000000000000000000000..80c6d5473362438f469a2898fe20641da4c843b5 GIT binary patch literal 115672 zcmeGEWmJ~yyETjpg0z%~bSNUNbT?8G0yiPu-JLfjh)S2zA=2HAfpj-F-QAtS`OA4cwOgt#GLb(#}%R|FNuXtjDGLlJuGP{aix3rP+9NYLzqB) z0Dj_w8iD}+h2W?pDSEG{mvsH!J<5C1;vy<;I$P5yZYm?AxBI%pbTK#{yXc5IS*TJ3 z%ab@(EdE$X=BhZc`8it1$P&o7Bp=c~pnHfUe2~H|z32b&5yCo|CZHcg$y7&&XFQt)WcE8#wT>B7zV{GODgXKp5lS2pYB8K7WKrRJ z2uL`8{fDHl_ycC7fAK#e;h+*D;1T(+sNkUfn+HLSXm8>8>*N0OMj}szsm1z}&=E!c z)g$8()jat7i-A|7G((ayLQPN>q5KyIhe|0-eShp=)Dk35l4Ddq$LGDfB z$#h)prDI@_HSJ4*h>Hhq^$iXVCiR`nzDFleGBNqCq^z85XkcLQh=e5n=*VfcuK=p0 zm2{|oz4hyY=Gn6=`=sOW&!3%cx-U$PjJU5eGBT2UP9|<(VIg5*E*G%W0UY5LM^L!< zs~4pH7W}Aq2(ph$jF4oXloC+Pmy-eyzuqw*UrvPB~ejPLGis^I;Jo-(_U_hIDT9*GNyba z`@K04GC?nOTKOb9!L7}wl40Ds+26l!4CgBMj-Bu@G4O-Dxl^1sXDKE8V%s0_9 zl$4O5d;a`SuASbzvkokqoe}taW6W zOZwux^fV#kW0K+I&UlZlNZ!vzKMo_~Tk%SciduMvfh1f0%L+yjr+ErE>v_#a!(IlhAy2VOaGP&xd2G2MGyCfcG zY=Y+zS0L5@OO(vnBFN&EV8bq2$O~2_%B@K`ttY>M=Q8h#W14LAtajNNpI6Z;somCZ zgg{HI$uf(`QO66QVd?k6FZuRB)}-WH&?n}) z_S~dMwGgbw<~q8xP3HZ4Q*bO&&DQDhmxk6Z(lT>UUHEiiaq)V?$aW7lt|~oOJ?k@+ z2gy(BBjL2_ud;^4#KtaX`JeRsXXVL) z-jfp^)Wy|xd#2r=3EsgA$LKlryT81n15E@d8pFdXu`IfRYS-nPMw&RJAccY-k_}E) zS|-b2#5I)IFTU2GdGPKARoyrU{E)8$Q?)kT=lX&kk{1I{k1-~(lWkabU?39B^XDsl zqF*_zkiikal)HZ3eFW%rzKuM#Xqd~Lv4nuJ`F)Gn`iamr3X5K~ASVcmB=2=7ah_IfS)NMnLbV0f<>{{L%ARW$_O0bhgBNf} zfo>Y|SE%zAmUuerbr{-oeWF>Y+dPw76_Jv{oEY%<9pbB3uRex` z#zL%fG^>l=RhXs6F=<|8DFs_I8EAeOCuK+S0$A<`p}+<8|4pg??N9JDQ=cmiZm-h* zeX!hBY0!#Z2&yy6LsHa?SiFxy$enRa`F}!5@};AnTTE8umSepaN)RfRg#P~ZBpnW% z*OC~hCGJLRb1aCeWIw7RO3H3(txuje3OH5KV^kUcE;H_`aN9Rn-30Y~#YO$%{TE>) zqi^Ng;;<^~kpd02Rm=Pty^dE+Dkmj{l2%=4`G_K7E%@+e`gY;TUt$R4NfbsHxj^31 zk`ahb+ObHDgP-%if-Bt*O}tO1T_p?8D_B&~5>9TKV#E3<3I6ZlHD;1^;M1rFTbj*{N+||~XC)<-1 z=Fy`?+M8BnrO;cSWPW$n>R$sa-N+vw(PcO+ck*Z|3>WL_uBJkpK$^vELNxKhAZ8yv zed;=X;)H1n-uUO`RT=M{uk7~{x4pA7%KB+!L_|bB!Ndvf=2-C+N37$RTd(__K+JK>vC+4t5+ zRp6~@WS#PFxA-i-Ne|oQ69GdULs)nBn~BGdgl^6U* z7c=ep@a4-FEjd@vGHC6C>9J}TFcRfK!Rg60qPiPXq%-TGVXD4Lx0)!^iQfdpFDg2^ z((}{?o(TDB#iI!vmPvuA#E5=4B}jo7EDef;+D?(gTs8&pite;NSX&o|P0CG~et4`MBk&kPscw@X9BrsmMWAXdFNk> zYLAfrO-~xLw6s*G*l4!N8-8Icy~+8*`J>El0k~k$?g`vq?8J zG?Z)<^x*?)@#P>DJG_ea9KIgD`%LvI^=y+?za#z{$du%O-gxySaLAHy*mq~zLdh{u6Vv!QZv-RC|`)G87Nr1PHSnPGSgF0{n#vg${5*JmGmJb1T8BIKc5gp zp_^Z8`rUsQ>XPbQ_g6MvyE$Kam7KW|oVbD#YZ#~ZVGq~4N{d)dtMRnOws2352PjW* ztPop25-2D0IuGZ(eOhTXp)``OcFLQszdO@NuUS<9*nz*b`|rg}(CY;e2g&Y6mk6$i zbaZsRiK{bBA~tB4Ro=e=1aw;Kr^?FCu21~fe9PkJalDc1^tCKccUM%~PK2zrt!?9I zeTdg}Cuw0}AqhJ_%6?;*E_-#C0MVz|S0AccW2e75m6esn19`>D%3A5!r*mCmP^(&? zzH^klEKkYFshTAdJGdOLyJN+tkA^kFUS91O_2tW&99I~Y16+Q&Lc%4#Q+^RP!$=bF z9MynV@-~c#YTMMujOugGUa7O*@8R>~t9McR^q5KI)PM40*v%>I0@NFwZGNpLRgh$&*bsNO}%a9(PnW2MqK7CPRTO>ZOq1sk~`yBc1z#bG_~=QC*41k z0Q}!e-&E=D(r8Cm!gaGPQq9#v{t@;VkHY&X(&y&f)VA*NxM9zD!ELs(>9o(~cAi#8 zeIef!nyEdhfXka%pL#8^U`&^VGVBCbL_%G!Qk%wORa4N0#^>a^dQOwM79Fkh;IQg5 zTlw8!_7RRB-~?-H1DW!*UJ8lUmUc_+xZVdHvF8FJmu-=+WeXS=wamm1Y5ULw+VOzZS0G;KPP5> z4Vl17p3%_I(QnL-aGRym!n<=a8};kED}oY&r|*UwVgL=J)ntV<1|jpXg3$H1oQ6|j_mHmZ~ANcAIBfq7{53w{~Wf;#wv!^{z*7I92#He@lP|H*c z-&yph`4!bHpeI9b&dZQc6cVdv!&Ab#?A=+w40zF>^U;i$e%_c}iyFC6Zi z)kF-irYZ%f1kRyUJN#DO&FzaQ8Qp=CNBWBkBofFyUXusr=x;~hDNXaJ!ZkckbsEjD=atJM>R{oEo}SZY%v`4 zeSQu8iX`NMGJnXN+2FU3^5sD7`zbcCGTT|f?2LeFO%*%&12qtFSI4Fi$j5B#g8k>XxPd99V4?7)hbyPC!b4+Y0|rt1mF z$hP@Ws{@f`DNBo?6fiuDg4FA;WTrh~r9#!WRoI?sGh2gk7X2lDc`A7k`N2z^Z9#IH zN0a%P72wDkf-$3sEJh1*`F3i=0CC#0kf=QuJhni5c#;olj?c2u%e$Z3pIQ&QdRxxk zcJktQlf(PM$@^r&w6nYW=)0vK@2FNG`p+}h=Q$>!8|1~)QCM4sU8CCq%PP z5@>VVwWFur;Y!a)gM=jKauB?8h&5|XDv7as4m@EeUuy+oM8YtOIBc6P_U+22Jk zUG%+5x+t>1o*uN)e0nf*+0@8{{#YfC({>_neds3Z{IGd1M^}eTCA$~7ZaI!QPoq2m z4sySJdlrtTbFwp?t^X^?{8x~!^VWE213F8=pF-8b1e@k-mrBbqc>rUM-K(f~V4vrD+c*M(7 z@XM8pbsC)(L%FzNQcJ^Dnh)~pO56qz$@t#zmWFH{jyH7cpZ;io2GLUI6(ug>F~uHnCdeoCOM$K z?60{!L@$7|&SMkKyQ2b!KuTKLKI(~qp`pOL9{{2rov)@^t`BC(Bcs3$eB&C06T2oi zexU5A0n!A|tfxa^WcBP0OI?$7&iR0bk>GuU^Q1hsqYcOK$qI{k&h+`=Tou4+s(D0wq3JL%X;o;*8@TLSH!b{p*!B@098z-eW zasi?*hpX){?3Jps{!D|r$K`glG0hfqmn1{B@33UnYtx#;YLBZL#xnlvI`XNc#Y#7k zJGrus92^>}+&fzpN`2cE*E9XxdJ6^>#unliPUN)d&X4$Mp>=sv!K7PI);6N1%alJl zSLA-!xNXCDGXn)!;;b(!@@{VV^Gyr6!(?zhJwlZgU+>J`&01zSyK(0uP~5n?*{|x8 z0DD~n?N{}9W5khkTuUh7p!9XqRR;8a`|1F%4m6W|V^-lk2JMoYHvkzjAj2c9Ekz;N zee&-BT^Y%eC7kxyER;tkP{51m9t}HOQt!){d!Y-O)Sg6m)7>$u&kzwA36clhy`l;9 z9JB4o$^-$=s-}w#1`XW=kp9lh{f6NJ-$7xh<%gG507b_>;66PXQV=Nr$PPR)cL zcE*ArtTo8Qzq>PVW2kc=d*;Km%d2auttJ*!^v>pkDJoodbzHU4uV!8AvdVXAt{*}Q z)Mv6aa2Mvuo_RlK|!x{iyq&NFq$&&zyZzg6*5 zbRGe%sGlZYYeB^OvKZ=C8yJ!NO| zSU!_*$!k{IjC^G?ojRdgL;39E{F#`;DyODq+=`BsjqMmzP0O9BzDnqA6Jjd?0YMhf zlh)R=Q&mMp{ktk{XG@i_k9Ui;>s=ndpwNA6S;b*7tO#IaGCfv`6~+9zcT0TD+U7g> zWP87`w{~jX~@nhXDb~~mFOqj zwl+-3<37ILB2K!RS4q~*uj@MA8n4yV=5>vt@#*I$e4*Ra0568wyaBX5QLzOZA8ADU z5g9;Y>NS4Fx;rsZk>!(lvwptE@O(4@dR;P7konMe9&8@-9ppNf?eCYn&3(oiw?pRU z9>*hNM;&d-Q`oBa!b(0O^kI4GsNGzn$LCKsIM>%ry9W;r4WavfxQ#Olq2`+PDv$10 z%vGWZe{iyLGdJK%x}*vYrdpize^WgE@6T|05rL$nl&h3A#XK<6s{_#dbNQqkKq2Y{ zQtsa{>1AC7V?%Xby&9N^x&rs*aadS-;nAUXE~=WNNCje81+Kz`D=4a}#<8-p#l(JH zzWPdoPAcHRK358g{rb%1j&gi&vi5V|t;t3;Z0xmND{Ik1`84p>8#9gdaJB3$!DEG8 z{27TK1OG-@coBgZ_oc*gj2V)v)Vh74tfW+7HbC8Yxl=z`?^;~I($gs1m(n;~sL2Ij zcIOL7UqCQ@R5+Eh}F20ca{Cs ztGDmbTQ|BV*mzxza+K{CmvW;gn{87MPgQvg#)HzaHger>M$!HD5zr%s~Va6 z5kNqe>`mPcbg|>R+!%9`83wiE7!$DMMXU#BU@DA2D5=Z4;@)6DOm-z z$#wQ*p6IN9AZ&5dAWAf?yFeTl8RNuCqja=Ng;bSKA7BazFOZ?eA^uga;$a{D1a}1pAhd1|1@DVxANKR!7SP2j|j~-7nXjk2hOG5 z7c<%9)AgNsltyE!MOwK{-j^4=py55x+M~#xP^s1=sutU?{qybRWj{cAoTb`F0 zyO%NNx0i6FBdPwk7r<=5fack6Q@$Hh$JO<(*(iLFXJ#`PT6RB>H>T^W0aIOmYihgm z^q+^t0}qSwmxpaZb-UObAb$ho_{)W>t?&C}rUkot(pRba<(Q;>Gjl zm)6g9po?;iLhAy?#>Vb=%xzck!XtSQ4!b#F4x6cQW4vCd%l?rMV#O((q}#s6zO%^r z;XLAYT8jXB%rfUeOw1!D-KMGB-zDCcF4b+0Ew^auh(A8yvgAp8TA3)jIvVQR7|GAp z?XVc(RUp)HOW$cc)uEvwINcm?!mPT8uOz-bQMaBb8!6Tl)N<_MxL6gs{kk1T_INkZ zwkg+j#=R}RAH!+ZVkD0zmMDSKIu8s%svoD!s7*cmjR5@@XG^(-45um|2^i|MS3x^H zo-j={AIOl9k(YPzso<(ldL5rA_2b2Mjw#$s&8fup{n zmR`*PYIEzUlwYSGixHN}ox4aY|AL-+kY{PD))6QG1y)S&@fQeiadX$!UYN?A%v)>L z+cXM(xoUdMthH8cO#$9o@OW)N0Hz40p!shzUz>Nvgg0QtF5ar80a2^&>Y!_+$=h?J zP;;#CQ}!ug^?mym3UC>Af0^6_7W&3s^mTUw5dMCuXn(V~d33zgu+n)`H56!jc>ny8<5iyj`K`cn16cAc%=l8 z?r?3nX>45FcQ6*nt`o7cN_s?P{4JY7KtLOix{u-Et|mg>7Zr!5YC6ubMKxD4BLxsK zW109PH=E7N5jEN4-}vF@#@8+avm@6w$2-ohRf_<0iR&_HE8n%39UaASEa zNNaocjF5*{di3xzogK(}AI%$uU%ENy0YSj|_CEGT8D}BCOibx3g=87X?aehDfVs22 zhZ&|2Uwtmx9lv2|(?AS|B-LD>zyS4pT)k-LYv~7a!@NID1k7Md+fMNncKE*htMWUJ zHZC@mlk=i_vuqK_xT`S>y5%OJutlC0M?cdb!ED(?os9L=-a$>=qKj$8+=#e~>oBN5uGAxEI@C_(m?q0h4 zOE-&}D)ZBc;z4_Y%>@tds=qJn5KnMmGgGc(@Ug&d^R13@w!Cu`k=YN1{+fLnc}Q?) zEPWm3pvUNEmiQIE7HUTHEJ=&kEU> zq#2NwCqh0A3;1|PzdjNG5@rna;-J1o)|OOc#*lPiFQ_{jl#K;*(YUsu%Ltes`WgZl z$@k;;mEAQ7^0Rxy?Yt2KCcj%Q<^XCBf!R|*anprNM@NTE_^R&}e#w$A!uR3|bukDR^<~AFGlfj;8Hpf8Tk{t??bZ?TQ1Re?^6p_kJrOF|X4Hh5dJk z5`aR)DAtsy7HX{zsXO&*{FC-uWnE5POw~EyfNg5|8!2kZa_*4>bcevd&UoiI997Z} zqAx3{B|dUX zV-<(FYsb>Z^>J2!<1#^0#l5jqbT=`}sZ{%ppv2Vkl*IDaNt#OyC^NyFSVsXISjRIUH9n*3Q1%;coUVfTG zVOL@251kZLfm%$27gj)UR}7+xAt+GbO(qTi#dwT1)hh!4nc#HsGsX^?v%arvZN_-N z!nEs07K?0Tq@~fmN;I?S`)`aCCR+atg_wNdv`$C?s#=7Gf(SM{fM4MLl=@Nx1oDs; z*S+)7>TjBSfH@8CZVD=@+7ey1eG=gY=GF<^_8E^^bh=&eM0~1P>I({Ae|8gzjEtOk zeKDkvf4o&LR%zIFLaE!K`)kv)&wFEho;Q|G`GN)NzS6DHc(IXxF&EH5RbAvyTw={O zP-U&@ygQ=}n!F0|QiMH9nJPxlTZR@V0*-RG#Sd6D{uAp-hFfvJOROeX8xJ}e;Nrnn zGl-u4(#2z9_BH!>SRkWLgE){3hG!Z*{atZws4e?TA|D6{N&%t$x+@Y$4+*TthK;-) zD(ywZX~;Gm7;xSMiX?oVSlxk{GF6S9pzpFt6X}8?21uKTi6;L;vcR0Yf#Wk^euH!mic;2{N9ghh8f}rK%&U%1@UxL#3 z_U&7nOnG_v(FiIrc&OG3#NGE7n~Uq!KAO9a{eqeKL;=D3aN6M*y4N&^I;im0X?s-r z^k};f%*L8On@MxSnG~B~@=Er^ljk^yaCztfHhIBd)@_}0JG5_i-J21|9%YdfjOP|L zsGi>qx~!0UQv~t>JSTPDqS{Lr3qG2(EM70~6BJ|pjkHs9$n4yB$Vo#(!($7?xl-ju z&*Nbrwj3DIa7qGf0N{A$2UK9a>N@`byi!%vKcDL>~4aORa-7acBNziEq=-p*I z!PJq>ZU(i&c2?(f7TOF)c~z_dw5R0_A*^STG?7$ieYm)HmH||$UNK|OwItYO+8fHf&ky)+7dQD^J3W3}d@6lCR3GPsrn)nu!;ftE--2GGy3wK@6U&M=3sJ=Hze}%ZdIlT2@GnIxK_0}JSl77 zc>#I}2tt$yeM-d!H+wQZmu?n@kkC-bDsy$7`OtH?`=B$1d0Rz$@7qMB^T86G^;FG| zT%6LmE|yA%W$Y9uXV1@&6t}Z^N$2AcwGG&A)7WM7%ur`6!}Q6b@7&{@ca?eM?n}5J zjTzgJ7+p^*=X8u`BqV`&Kf%g+r$pnMiu4~`yTU!W(vt`Bi`adP+PWbBb zG{^X2V^pTBA=s{#RUa``)8|)5n%7!!S32Od-gj|LHOQ(s~oG`iy`>5?Dh-da$i{`RKF(&`JxTyyQT*{B+Hc zfS9=ft(U{_Pqq*2!Bd6%CVRf<<1XCVbxvkNkQ}`> z?<$jgmCbZfcB=ck&9TB|Bg#EmzwvQ(xPjwz# zXtUI?nFF56Ov!M#RWX6b@i|O7aQwWd%Z?Vhmu`^?P3hWfD=MUsZ5oK6x^;@u1wx*& zG}cancKGP&%q4}&sg3K3n|INfeir#&(w6ul{W%_Y6XA?=>XrFs*;jYSpMmf z;FDf2U`{xkFt)y{00^Ukc);a})ve}Qv1SN)W%%a>4-XHLnpC?xmEAQKm-$>KcGrwo zUr{o@vkXhqS(#uWTi5D3lYWN$_Mvtdo8u^{0N)W2?YUo23lu#D7+J9S_k7Do#N{>P zYU;6r>ghu;=Y4`SDDDbr9MS*iWJO)4Fxa8xSkVX&bP53IeBa-R%7KGu+k9Qc+^}a0 z^rQ>Jx}(pS)@RNEJ5BY$v!W#^{n&!zy2*U5N={BrRg3Z|SLr(L#@}XFjMDaSbHEjm z`<&^I0QU>P9n6F)J3dbYsiE3v;IX`1G)VsPSlKtXB!}-Y2zy^G5HP5&^>ED&1Mn?s zW*kT?<+mf?vG#|=M&&*Fvh&)WK2XV?$(^s=Zw}UjA&qMuhYRv5qM_|WiJ-ep*Klal z+}GthPh~g%o^T_@bLVaJb2$gC2-?%uJ)HAHdQhy}$~zP8!d8BuLvK?%(3jhEonLGg zb2uzXZO=6F{Z^H%@q5gDdo@|NUB$-^f|E(}q_J&CQe2#SEL}T1#Rfy)vGK5%50KbP z!<4?2-XTzsM?n1)NJ+epks=I6^6Yvo_bP3sdD~h&9iW(80>BWo{ke?%uF6M=guInr zDj@DM0=GP_rg+N3?H4~ra2;|`)nkzvPE!FemP_QM0}j;8enUe;4&#kBdX2KcAbvJ+ zz_(+p14PAIl7+z-0A9y)fGdO<+u0TB#3lLS2Gqg7+GA0l`T->dnD6SeBCoG&>=)U= zIMLX|Bxs;hL#706eDt~Z4mvc-4y~jl74^d93v=M3y z`{CVAgDa}Jj+9;5c;*p?;)V=>4k&|#Q_$5rZh-Kf3w}(m(@?`U25i~M+i#xBB|P)Q zPYDv%!BCe7zq!852iTNHe!1hCt=O<MZ~T9Rl8^ zVqNGpfdJBhN=wnNPb^L90S~Yos(^Xu!0yY(cy+?SUks*pfZwQm$|8Dhn9=hYd`S?527hNJkX; zmi&}!sfwxr?S2HHc~X2UAKhw@+H5ADqs<&aR@*J=35d}51CPx0BE@a{D_(`2Vv#!j zc>TzB91GLWZg-(6cs;kkZZ7^PKOTmhYB%TJ$&2^ha^}}Vxvf+8JuE>Xh?U}>p$}O8-?Dgx{!Uo28 zNN{u1Gd5r=cpW#chy!jGd7iQS^_@YTiZm6=f4f~h%kH+PH(?5QIZDaMIMBa>(=E77 zz@oFFf*lM})uU+@M!}@sXhhr$*aYVq4ts|H3pGmB=5RfQ3IAR%<^f~o_-BiECNAog zG$qh%f&Y*h0^c)H({Yz@TsLlM9z$4O&0eCNR^`NGN}QGXU30 zK-w-_VDeJU75ZBDIZiu|M@#GZG6_}%^iw$HdROZ}10izwBqdag z4B|Tth;drL(gqn^Bm|imcT^A z3y%1|-tqC7LN*SQ~ z`CvH$J;(#FJGl9Lf@BYVzKV5D<^q>nOFsXGbI{O*- z)#VUB2q6)dO_B*P#1<43@P7>u!EKjssrvGS*C{7~-FyMBecGEx-56A&Q8qEHy9o%| zw{PEa%my-;@rgjf&c}#USC4J~Aja|&MpY@+(e}PLdYi~;{RkgFnpXK8N0^EAr`wNyt z{s?-po(;fZ4A{sEutgAm6o&|aziPI8GJM<}kL`Y4xd$qg#r)6r#6XS*rlZwjeIgWu zYDB=`{{eA;Y1i>{0^hJSYdJbi`XGSxv1}4acfNfD`qp&48hnq4Xd3Q_12q!BH~}d& zVfFdvIJmg!yv`eI`)w3s$b?#ym%9x%U~!`VcL+X*X)h`8*)AVDeGUgxGcehN*GiRY z8|`@dot>c{f266!N|JFG?u;!_sg{`|XSK4~s+cdY3}2%bepD3GretQ8hdUlTj@EEX zqApZpm-4~r8vOSP;8+JVi+PwRL?{r+re^+u>8FsS1lrS0Jq^|n%pVTVKit2wG>P~D z;!6)~EU@-%k)pBV!~P1zlgBqBz++7lVsIy^rw$+IZ-WiAN-Q^Qi3JogEH} z6h_LEfkz`_#CHx*8Jw48Q{M;iiv~v|z?%slkRQI}0)6n;}jSZMN+2NKFfN&Ib6xz-J`UJkXEVyM8F27nO#%{fn zKi$Amye5#)aSw;b|IOHQ;VG%|IR?n7m zmktyK9@+`NAYn?R0^(m;@j7|IE@1qQA@8#X?oc@(*#W5Y*1{sM?V1}0K^K`( zdOn0UrJq1eYu@37>#3uz;a-+d2ximT0O9`(07Uu-Fc&ZAezS^lt4yoVIb&7g!cWtB z&Zc%*^jciJ{iNd!{aewKF04!5bUS?lI;G$1x;0XfUejD`X&>GM{r}|W8Ks_3uttZ^ zB@aVA5bXs80B5)WW>4=g?~72~Xcd9&_87Rwwqez-J}cPYO*CZ+wChy?y>bvt1-0kP z&!=%O32V+4KEe43e0>iVJ9%u@tbp++4--(V^VW`o|3RBkB@iyDI(_Bc3(|qCS2QTO3`Hn@F$-C z@PERemQ*CLj2uPKq){d=#Hd?D5yv!iayuOa8P4}!L)z4(ozk$kbx_cCt@C=1o~NE0b!ckb6+h-)X?eT;1lmm@W4JX_t6V+f zmU;NyQp>ABAwBUj@>lTja0Jzc6UbSSMvrWWvMKlB2aC*FzJ80Z_#vYc92RimB8SsN?+B8oOF_W&oZYc_W1^l-s*{Qt7 zr-vC6F83rp5e%gP8zjtS`@jJG4<;_c=IA;M7)e}by)xxUIJr;14J&6S$zYyO zZ-qX*6wFa9&6w2ot}NQ_7b#?}XQVu>zS7}4{4Y+e7=iSVoI4G-PcVPC`TTXMy}izD zv*6RGzjB{&%u(F-Yd3lz8*9w?I|*tqAfhe-a z3b^Z(eK6SNCC)K-C0TCEhkjM9-Io36= z5~7A+p40R-pdv}NkgMp9R^gL0X#rZP~ZOx8JQaRtvji zVj1$SY8{}`fv7VC%YtPP2Gtzva+6vL%EXit-_;&8d{VJ@GZ^F8K>{}Qa^>F@PP>d; zW}0*x-(|_fpO`lYf)fsZE_Yy#Y0C!_Ywdf?bWlaoR*&Phw%EMUI+yU6gJm8>F?*}3PO~}p-NQ63wpxip;u?`n$ z^BFw`LEWB2k0?Ruig@yU@tlC6liXv_;c*H5L&D+W;!h-;PABbETtoSg;+=-=oi37% zA8S4@cqVF@x2(OOm7j>&`+tWEI*{-6ALz_=^$CsjY{M>%sbvq`cUlRZXT`A3*=KgN z>ywr~8qfM{4sp-8*!0(w#ucc~TXTi=y>6NL+*}|X?YlK(da5OxGN1DHC3EXgZZ5CK zQBL$T!`O_`n*-T)mIjBBJo)!QmlPiD(lMCTP=>%OGy{bHf|qVodO~eimYt7SJdB`g z`G#%0@UWuv6|O-a-^H`$YuZY+9?DGA(ufZ_>m=@HR#Z6X6DaU?)Y^ zaGD4sS{|r9cN&Dwx-i8b{cV z=F{Wc`^NQ-Og35HYn!!Y3n6$==(U{}*UP0HDwA}_IepF4B zd}=%SRLw+FfK)G(uzlUM6io4GcaihyCqhQW>fU7Mt+lt%z~%iX(E2k2@59A#a*95` zB4l3YwKNvNOGZj@RU%VQhcd>$Px0;(wj3~%9rp?H`@=yvOnHNNN_jqE`-oD$8`M<`G10|5YTVS4wY>-oIL zMSz2Lyp}`AKjVWu2*Zm-M}lF{>yv+{(kXvOT6&)m@snztp8&qd1NQ&EFSLF#ZFK5#u_+T-9eo+2-I(Vm14Zkh)uq&@%X}zu7B)4Zd{*r%Y{FUL6S+kE| z_~Q1x5sTse&f`sOlSmIf9T5Sa9}Vtx>ywuEtGr-gCKpq+2#Mh#h_rgAxilDr8?o^^ zgRZ(zZM~V_rA8%GKuF#3zOWNQpIZL&=D%X-jB)RHp>@M&vry318N&GLEs4vHN@RwF zcUTAlJQMXEmEfK6H10P6OE8?vmZ4MtYD=Z~O$ryR6UYL$2zp2AuQA}K0?GFl7*H7? zM2Pl3UIsta(<~M`Uy82d4_-ys**W$529RCEWcPbvU*xUAwH<&F-|Kqe&VBvm%IoP^ zBbp^sceM*u>ud6Mw$FS`RjUsBlz+V(znMrQdqsSUGW0$7S18wo^P2~k;Ucgx)`5Y==CrNeqabf80tSgKoO79y4q>2 z!Xsrh`Vhygy|Mb0=%L>hvXy5Wn88@iwfF&pfPob2#A8Od-U|j9NxLj?Qe-xg7Yoc2 zNs9skz}NJGejNzcIr;wXIPEA141BZSrSX$_-B~G2uy5&PW0?z0B|rB&b4^it9#Ria z`d;n~opH?`S&p+bpCoupdlu`q8I|1Pmjw3|l5maIaadmc99$Zdw)Ef4{O8$8#pPyJq zM*^E+&~v#jX|Y}LtJC6C>}A&^^O0>7YN;sXbNa7Qlet9y;omR5Ad=nQxO!LGr7L`@ z4yOC1>!Wdcvb#IP?F8Q*1p9CRLxADXx2`?75(*ZuE}cnWdXzl3c@|=t@)q=eQH%NAP>N#c(Jl+@uwA!sHZ$B}4TI&(lKam&i{D$8 zhBT<)P0g&9pk{>>X#3qPi{8~vxrdbe=W`;>pI3V*X=LJtyIGq0C9p_5hC&T*)#k>W zH=U*l)R2y*Ms8uV?4aUx?gX7d-1uoE98G%$zeGN*G&$bD(UqGCylwe~y;)1B+nxA> z=)X-}SJaA`RvH=x-&vTRKYuOjb0%`Kt3?cNbYX~Kp-U0uSef=DP`8fSpN5+O-Zo$3 zH2NqIE~X#v@4VC=sfhaZ$uIFV@SO>d^{n`$ zZ)tFfbuEksJ{cipy&_84f_Tz0MD8_+_*BnU`yr8_);=1V-`D;&4HM`^?#}vv*a>B) zu62uH`1-p3uPYAH#jD_!i%Lsp<9t;gK3$i_CT@?4+0TWfo83&winC=JX7lDbs*O+Fu zyLA?T;MkJ~4d9>#`a%p#P)YC!K5n?s%6Nl&xenlHRVMo=&kfyZ^rX#z z7s7s`ZTfmPzr(@(C0G;+%p^hl!3?K7K;%5q%9mkz(I#rNXgfSBuf3b)Q$cs+mD?z;FM1q4k9eW0p`BRRkWUeok$6v8{ocrWV(^7aX;m)EM1o~pUxji$)^4_4s zd(se7{qYq(s7L@P&32`bhW&?9xzp*vm;?`6?w2*rdYh}R_tMXR&q3V-&Q7pS7r7mq zqRr>Fm!=%5e|>TMpFnY8ngNcG-d}34o!?ci=*Mv>DMi4rs0BhP+FJpfTZ}LmZY7IX zfQ1bsK4baQTj5q9q@{iPisE+L}wOR|vYERcgu9+vpPc$?% zER>p?IBh4Fs=14sc6hz{E#qeY7^XonK9jM%4!_8q6t=P|c-?#)w}qt-<+#k!oz8<) zy7rMnvDS~dPXtT}h^w+Q zYeKb}EDfU&W;9^lL<|OYkyt zda^SAPt#?QP|$@T=BJ)014d8o<^+hLoBnW4+BJ%rPwf)>ZX#e@6xaff_WHN4NF!j~M1S5qrXnCFXC_W1y8r&fsCdwOJhIH6%iwvv@mgRP7wF5X9Z2%iDr{-fqEXMv@L&8Ev8 z$Eq0A>RBO8|79?&Ny@{V^|`cevYJg?rHw@MSAtKGh+i25cI9F6Ttg{wFsLJ`a5}sF zgauK@f6;kLNpuPMMPwm?a0Q+HS5f7z#ETMGJ)~z@V;erZ?ru3d{ue6F)$p|4WfJLT}a*K zPC&l~tbz!-j^Tpd>>Ht6QoLw24X5~%aZjs`KUj;19;OO|=G6b`ilhgc_Brss&V(5m zPT}4ycdeG4-a413manjA)61Hr!LcE-^c?)NJjx0JS>Qk(eKRf}^}>^qlIq9eno6tR zZGtVU(w;v`nEhV)tP3bl=3p;zapf|>=sE<_kO@|?Z=Te!!CwM}HXd)}`;aurR9X;UuOv2vEYJ?L zg4HBVusS81b-(MKkMowg^X9~m!oaoX6Ie>xWns0&pXQt8G5ba`u)@v6;dtXUaZ1|L z*fQ5fKE&|!b&b$V>@c_U%CX_i8P7vKqoAXBUvJ&!>x)f~o%L+v4ovo2jPb`p)1Id% zw=JJP{HMr}V|X3R2g?tBFD`V3RV9?$r5FF|rvTgxl@7sE%oTj#CgG*N|CTZMc8xoe zB3QnHkDZ`F!1O5Yl{R>xSK}`a<9*mXuRen2+j8omf;g)-7&(jfa|kg z*mZ#MCG*=;>&BfPV+*Y!$QO8NbZ`WpZ@&AM!^Gw1aVcP@j6L-=F@DKL*%TZ=KEP(H@{j*T`r$>Lh?8Am%AW!Z-|;dw^82wGX+yER8OA) zWc={E+zrV%_EQasuNH7ujbD{@G(;yP^pi3eT@ongL@=J*+7PX;^SW$(aoHKCy41Jq zE7E>fZ4%n&sduT*1H~MuF<0*S!llrPhkwn(X!xJ$lrIWm+z0RTtoVy7#1AxwP|!$< z8cyC}n=He>?@8%|1V(tg0Y=_c&Y!GV2L4DG+h0y6HugWEprF|M047!3JowZRr-*UC zU)oRAG^BnYYQS3)vP%o%zim+SKDd|z6eV4rjQ8xALL6K0;6qAJcGR)^HbKt*HkHAr zAS`3-WN&ML(}^#BV$@Ybp}N7HRn5(vr5D6Wu`QF{M>(?$@@K?qCCAX!!s;VML7PoI z&BO+Flg|c*h5@19h1(39(=O(wU5}5_M9weLW_Q%$+>Aq*<0|whI4nk0V)G!73iVeL zClbIl?0o)-U5$1CkLYA)rjQI)ZFxJ>I9X@>sY$bLSbCg0CTZ)6GEO(NQ1w7I#r+=< z8GLjMLRVj4uAE9#FgWh1{5{8`CqXd075*I@;g@JYV$lUN1V^H%*t1(OHVmLMGC)&C zkj<$Dh5)C{!s@(|REC}h3}3C!%}3;4hmq33Ed1fX`(3roww7a2Lj5hv667<`1Y#K% zsUyM>dw?^?5_;Zhi>tn3t>xvVxD{^rRrf^bp!~%Cq~P%7FTt%oDxZ%$k=iW;t7v$x}%az{K2L?Tx<#g^bGfN|F3q=_K8uTP32U!a;EffzQVN=&O*CRM&7EtI!7iN z4zJcya1xh=mb)fJmV^j30$vOj0z-uhO|5?ROR~t(&RKK^c=xRJOwpi&eT0Z|h@R73 z{vOV=<PL^s%4oG&>E3yn6NLTACC?-t1XxCDSi-CxhUZ<8~Yap zzT$LuNf)coSBd`yDbPE=y+~j+jXHbO6*nJiWFZ0bt^gb3e#gEB2+Tic$48<|18?)> zv#doDL)$Bxin#pvU}?0=ECbFJ{0t4|c)3-yQAlH@qA$#IV!BMnI*P$HLWmCu2IOE0 zfSKeN=*}@_WiCNP$jQ|en~=81Z`T+D%U8ZYE7R?ZAPz3fAiwY!HII$zOMh26UMAEg z0e|o#JRmwNi&6ROOqQs8ftfN*Nzc706|!{AHwJEZHKxb4*$=`zx8vQR?Df`!GC$e% zS`2(i8!vaDsXt(Ms|9qJVaTiGdbr|qdTPY{!r0v2y4c^@I0)NUEkjgii)G4>$cUF8 z%aYj~aBE=u&2YDV_{jxDxlonOTuXxEdaN@rU#k+5f2~Jnu$E+qE&(G^#RH>C|5?CV zRyKvhbzHw)&Nag~i?q?rHN)cOOoAq%>$XILHouJg_T;fSGe}UtFW=qP&irN`nOAo> zQ`ls_!oV(Ypj8rF(*GVR5tMH)GGWJJw=jI7{d9()g1UGcMflx>=$-v15z)xaOXFJvLC}LsY-~>~t&`U6r$+(6+5b>SK ziHX5WKr-W!CeHa8Fd&iq42C&6!9JKMx@Tz*C3&(*5JM;OWGik@Gd6`or5Kmnjo{2F zK7r0lI?WgfJIT6C-OcOBsxR_QhT_EA;ziNv8G-?yKZNDzu66|da*{Szqq6hZuC+vM z*7J*~Q7uwels8CE3em20IMu(t*vW`@uexW(X|(!{RUZ4TnwNyH!2`|E$!gI|*#wu( zx>PG#L{du=6N5%tX57G8Rkg|Ew1Wh7V=xx!0*^&NmX~)EM-Ry`j^YQQf!uNnVnI7v zh*Cf(R<|h<4ZqsP#&7X|k5HEn_VRG)&33Ld3-5XXob$G(iX8zgO-@cZHs&lhll zxx2xQu$<49Rap_~ zw6%^+v%UZMbAA3&X1l=VN~h#w_?oc1V`CGxj6D7X4(It=HWRfyY<3-)1Dk{2j~~d| zYC#y*3<(2{`GP*~p1}Jo`aJ&PC_j~-ud^1lv0;2X{oy@xEn#?F`;8AI88SH^X>EWi zwtY0(O=hP;Mci=oh4QWEa zy|y;KFzgQ^L1kSjZiGg@FveziJYyFgf3B7AP_wox?+2kYVfLC>t=sNs!v_)B6nGwVi^VmWR2WSA znM`5X^OG}$DB?>AqDgeFUCn==n=K-~D2DzP4a=#oFIUbrlb){}su&gg!CVcJK~ETT zv>G_qIKH;oSscN@w>-YLasS5xNZT&=Ll{8z2qKsBQMJiw4%u))O6N6v#2~AF7yL)F zi9VEe2@*eI8Nb&8Bt&q~Dh_pa4j5};lYjA?5BS38Y67mSUQ$55z9#$#PeV#Vi)NMl zmZ03F8?i{zfMDj=J33rej9TMb<{$v08V$q(K5U6#D47G@@8KbU_Jj4~eGs=B2CvaD zZI_O|45)|RFtD+OMtV!BOZ`}CX0kg_`(+wvFpw0@Wi0men80NqVi&mk=&b#N?dK($7m?-`wD2>dK+vC`@cpyfZ(B zmwL~IWgz+;qjHe&gJBzZDHT(M*2-ERLlPBpbp^o9-S&PIg^vs7=#O9`Rk2M2uP5T8Ud?pH>OCg3u!vZ+K{y|biOfkPip04Ygvx}_aD+&y zAH-pc{h>BH-RplDY)_R4kWlIrpk4lCC+>O#?2bmbO;O~iXQRA9v=m0yFyra|lb~I3%%h!etiW-c&b6tq%#U%*E2|j22=ZEwxx~iT!!I-EgtL zOvrurw_WGyR`n0uY`eT0TElp*WZ>c5RhCjBvy)GZ*4KZ&e9tv_82G)|R?_8RW!cT< zpRSKtj}p;YZK(aHRQv#6J{lU^0N|dQ!~~=C%K$zwV`icZ_~Z;&(=htajVk~ryL3d` zG_!aM{dkn$b-w7ltj^*jZdguzZ)X?m!w%~NXpFu;p^TR%Z$DSVm&{e8t_~Y}Mhr62 zQ;La^FfgHbiNzw)WM=ZsI-27|kDnV4ca1{7@DFH{I3jrURxLK2*n)|ZqtSjJD`^4* zXvn6Sr1|=z)++5RRplA$U!ABep>N4i z{NIc109%WnLoS_w}oUpt<2pCG6nfj<=edlmYPYa8NQ+u>%?pT{v z{&IIEXnaFzDU^}CX)7?$uM1le`Ire&r?oX5;CS?d`uUlT0N-Y9NB%RT=~N*m=uO1s zsbA~75XU5?-ivrmIoIUr$z(J|J7xtCtmkgzzvYeCUpm!nc*@!0Zm+=z=(;qXZk@-o zcmMy9@pxSO03x3p8yx{4P!Xd<(j>ZXV6O)WKDjWaq@)xxHFs~_gNPXfSy@@FCYLdN z`+;BxOfWGY%!}aHkdF{Uac(xDOyJWa8H;Iwz^DlLB<~cz&ZhQ*&*E%@<8Kk_GO#2p zPAF3-#8_(fERv***=o66@l|Hy|4zv9RiwrYK~o1*G&V$yvX6$Dba;qwYu@w&scWCN%*NLe!8?9z2- z5GRI8Ri=ti)6r(X=Z$c5FCWkN-{Hp5 zsl%WR6tv`b&%C#u2Vuopeg8Z`LTz&J_do_2;1MVkLNo*kvpyg?5r^6sM`}PY9T*1^ z0O&Z5IH`SjeY$?4te#?6meA*@{tlXffx*SyodUZnPxdE_gPuX;L<2u8_Wao?Xj*gfob0Z;GH^=>4oOqyeWTyG~8S~cetep71 zkl=!5q2ujK`HYJbOirhJC0^%NR!@Hr~}eV$%_ zgF@IOJKO9`o-Sh%jw3b@q19@{2v1_2+z@>m_s?%bL|_FFt(4kLE?+gBWaaP#YNXFzU!LZ0eIqP&N-DVN&ULW8&DG)#}(-&0)kuV_8DJqd1#4 z`&U?s)G!;JT#u0wOB(1uTn8YnTtE(=P+T4laH!*H3m_pixagr{znpx7^(Ap9#oe3^ z3mfK=gBx#FS52ONUcS^r!b;aE(@u&OCW!gF=-e-CHT zSCQT!1{%-g%VRU!W!tp=`%w^sRiK6$N#hY-#pn3Q@KYjWzt&3ldkP23UDVtEG?u6+ zx|+e+hV6-MB#Hb7ZN`RkeE&vpc@Y-2TqudgBQwLMooMq#BD5&Tl8&8 zlLLb3pB(hVIfyspOSPRtSHXI(G$&7kpP1oHy(iZmn_0|42G!PajO+1I!-uI7Z92oD zED9N+R(?PKYvxG=ng?-xwO@8Q1%hbY^wM+HK=F8^UTLiSJs3cq?V?5UmrK8UMT_u~ z@kVO#*G0n>a(bTYc%kmDbg7%OfyQ?u$$?v~o`(@yh5YhECgfu!G z4*RV5{W8hLv9e zYk=9_8O?YB{9A^q`%$44$gq`${h9Yiz-Se>@-@osdl1<2gCmqq>G{t^4c#7*ukvs$ zZ{+h2PiB7U^#U%)#weaAe_*s)n_IcRV*{$;aR;^1Xe4bxukro?>>&{(w6-pv(~b!! z(F(4GVGR-LHn8L^o*|g1V+g8Iu{Rlq4UMCFVI)mw3TaZJRFMh_bL$_^vHhq$Qa6*z zg((bO!s_wYBKJhom-#M4l8 z#Qu#0w`At!D>yS1iD2HBAjwEJHALIct%uyRR##rmNDESqYh%j;(arMAu|eSg#=h|@d(Y?TBly4bgepSx zXmmtZL8x~;)}8=i8zD5zmlFp?{gCC_9g|hxBMv***dk^0qiKx5gJu@zp4YH%@hNFh zSr`e!<4bh(g*EhKiO|!k*#wbxvBnmRwJG_QxsYrZA2^oF<+g zv~a=dy~982*jGjDS6H`vLZmFHPFEk?Ga?nm{X%d-^V_!E`$}?aMD~&I!*@!JCUzOE zCU|Om@qceAsJ-xDEHY+_y*!EO>hnL%w2jGY^4V4YbLfolFjDE{mqYxUtsolf8Crr1 zR{2JZdd&u7G4u(1ET7j!(G$5|9TD6=JlLMD3I9_2q2=#MB3ETB?eFgoksy-TbIrs= z1$t6QIz9txIWr82`-ePgNAm=k)gbd0v7ifVo{Ixfbs%WNDwY|K=^W3vvT>3M$;-3Y-v*&7!=UpQ!HcN)O> z8#LYGV=0YqHZ?K$>GJZtGv9W-?G8BP>lO22Agq(j6EkP|l+|W`wpsFL!>;*etPqXuiBe&!#!y?zZ;_0KtHN{W_+V#hpFTsp;Em9A?Tw;+wNg zNt;-4ZrSP%lHk`Gs4?qgFe)N^N}IItWA(1~wBcc0%a50AG-U%be|SC5VBZv~_JYUX zA5J28;8F0~3WN7es9b9tk@xphsc<*7^5gzKD0cF7N^qB2-ShgrLF38@o@@+i z1)*p{O>8MLObSs^Fa=P?qz{GzCB&l*cZo(L24pG7#J7@&1V$=I^3N-1=G2?y?4<8v zG^!OOPVYa#qkYd#{MH5@y49n#ub>V|U~Tiys@(4LT?|C_^N0J-Fz7!Y0uw=9S2b)< zL7Lgaq}xxICUCm5dR?!+8fwXY@t<}T7S;=ZU_!^HAgs6x2mq(2>!PIo)CLjo9)5f- zF6_V-YV@a1?#2)d0Yf_cKL-0mqKc(*vH&YX_*`(l5XHr(u}1oTFD(ho@luO$#=&d= zB&Gf}HB9-t+Tu2;+w8}OWAEPd7SDj+Vs*}r%ESD*Lbv0)o&~m_IsW9mo66nD8%(ra znV@1cO;tF2E{{NsCS?~~o}Ylt&T3BH5j)HCp4$Vy&gsx*YeT%M*NZz{V<<@?^J5El zlebQLh(8=%&psV9CvMI_%-C8Fd5@WV;PYH!MNA7H5sUslu;%h$4ioYrh>`k!gF#Ks zpJta78djgx%KyvuaL3VX%GayIs_C*k7nr0Zq|zKeKQje+f%9hx${vg1o8o+}Sw}@8 z6J$dUVGA5QBI-u_!(DnYxw5g9*00?A?Vpc-^Q@=NpPp_;#FAO*5HYCz2^De#dctsG zpVH_*b7Ih_qd~rkeRvB~JgDnl@@^z;^>(sI_i0cSe24qjdtL5NGDxIvF0Oikd{?OS z=^ed>(yUgQT0^Csb|?k|eXt6(ES-3W1-jGI(dU;>MXIHgbj@j>+_FSDsv5%!ur8gW&M0Y*kR2)_W`lPZ5SzzKgMSQG=LO4-;UkYtBmd#s7l4gGW$}x!v%yxbMs-o@hmL`sa5x3)JK2%7UHMm zBPo8lbzz=E*8AXo6?4}O0tI3>&+o#gA($O4tV^0;eh`KjOG9%Uzi_?9?f(=4WSkVR z$O;0#sX+$A8p;P&9V&SKeh>inl$2y;WyJ@e)AJ7We*McJ{k|WIUZknbP|BOnr@KyR z_jHuM;1fqa2+`s2-&iJtffHF-Y4J2!O7|^?M^57msKlg(KmRiF{aG=o)HlN^| zDO6UGzJJ%3q#Ts}$#ymOd=(ZghlJ@vC4<${V8SOHeju(PRDtDbYE`DuC^}{-DeP_- z-)qdxtsdGMW(5MWQ?EsG(1BKb(vsWFY&_skS5ZN!w;pz|!APVHG-o9=PqV=)RR*c# z58KHZ!uFdAQ8*fhy{~a>&im14RRm}vg2TKY_F@HSIXzZ<%5n1B7~NL|9CjAe6Gmb- z@+j2AN!vwH6W` zzf4Zo@|AKz1Z;FOV?C8})Zw7XRTC{XZ@4|rvISIDJwy8$Yf3^BD8InOjku}Z+GnWDr0JoSI}ddk;tq*~m!zuP^n)Z|5srtzwZeQ2UDfY0l` z4(B6z6D$WQJk=gH|MB#AJKbt#OV=v_#K^Cx*ME1>K~Pl)z%vjpX&1#|yNLP5pAGZR znTfVCbO*b@Vk@Da0LzCeGHwe@#V6+@yfoQH;6g?IGxA0!m*N8^Z=pu%sdxx81S#?p z7~x96UPE&bvtvg;mkC6?xW~;NiTbw#<}bqu&jai?M(>FUdEZ(!S_9MVO;BCjB&F)x zcOyUFF>@E~zfI#wC81tNhY1^aDZMv^St+lTtHoh4)3R8Y99pcaPZkZYS*ibW&T;SW zE7gS<0-!tq9Wp6bH#5Wxx%jc-3sNE@bi1!_VO*bdr)TdEKg{LR@JXfF@mx$h9rKB5 zswv=RqUPbCbcO8+!}g_G?;-O}#}Nhcb}l89m|`GQusLbqYk4+yUwI2E)J7ZPJq~vG zb5m2%g^xEE=U=oM91tL#so|lWIp^E9H*3P7-=eszwb?&fg<;9a=>EQaje+K^qbp6VCQPPILGP_WX?pUb} z@i|;j-VLXA0%V82X6L>B}0njkk8^sd`3i|x-s`o%!QukN00<1}RI#u9-DQ#iOW_ME zW+w+bh3=`X+`mz=G-C_>yUCk}ODzPJu+{XdEL@;s{)+(5NbS!`@J@5`W3jwoeiH?m zSAn5^$&?7Ir@$lnedA=&7!brR6zX?7V7cZiPErReOcgh8Ohc(*ntoPA0jZ%RXtr8h z+zPKd;reLf=sWA-JPrH2;JCY%Tm-qkeU(Z{x!bzPWL^+L`79Xy92P1|<@W@6D^C-?<$zuhi(@rnCLGqrM_D?J0Li)?j3niJ4ua3 zpArIvmz9aa>DfqPWbKf}*;$@W6hLWG-Bneu zk#YTj(laA8=msl3Sqmb+SKJvUvBNAZ1l!|mxF7EMa^7gxOi3pZIc>`ft&XJd5`KQ^ zQDpK~$nQmMtokQs`nZ4qsuI#LvwIa&z~$WEKgjoV;BAU@3-yqpj=q3y`9t>i{osQy zsA=@4iq+kuXgqV}U}wJ8gWqfYaP1dc+YUKhya3<|$pQ%mLe$KL5fHhC{s+ zEL2jf@!|#&3Z&z6$Vq!LbF>#p^ds!>T<}x(`BsMa@lwYyF^!5A_b30Gs+Xe0QpK7% zAqwGzlWH|s?tk-1`pu>1>%Jg2EiCve?LmZ-(1Z;Wnr(;|xVcLZ60PieEf#`~N=pl~ zZ4=pz?r_OvT3HM4C(2Z$`@R_ihQfArimsefB}ol7;eN#xZFYOqx(y0eHi2j)zDH3w zfRX#>zVG)2xPM7MbiQLtr?`1;y9c%#HQ#TwJf_XcTNb~OY z4X*z%xYNnGm%2??RWBE!ZqE5Ja{#R}KXH8cn?qN!bLw!O)Z{>Iqc8UNdl~6U^OY)J zY8I*Ygj^nKf2>(&`yJTR#RWFp{*MI!B-Y8SM!k;Ylhs?jhV->T2 zzzCoa>E(ibx;r4~4zz|@lyE`!Rsc$%X=-k^G}G~0R-psqCctn6=t@z5MH?L;L<5M# zTNCZC-nue`eZW;s8~h>A8fCKxzcFGDc&n}EmdBitLp(%Bzl_z@7r93`qx zUVp`uO$7bEG8%65!P3yzXQ+R&)>igveJ_NGwe&kXSa<~xcqY@C0psUdF%^w=fT-$= zrT9S^Pc?DwV3N*%kZUTIWS*!aCKx>K6s@{jiW z_iqbPz(7tZ+dcGd?bk_+dezEkJ(Ji;4W@y>guEb9nNoowO;^=V<}Wf%{Y|xhRmKDA-$5r%vo;pHQ7C|sNxQf?o1rO zu^>ccwk4rY0VYC~>PQK+nZ4NH^laHI-U9W1lzpI!`B66xdbcw12 zm7$NeOlCQD{aLHfLR~7A1ql@O<=td`31l$egnW+AJya$7uGz>_7_^&A`euyeY6JN| z$_8JhZ&p@TCjC&z5n##@G{wN3h*d!>P|U;HQnVIT;(kua8a4g&>C?*oe$lnkoLDOF z53m_(9i+T;C^ZMb(WcHqU;r}J0iubbJib9LJ>-AsiW|y>rr?7cK?1XYvJ2v{%}h_r z1iu8VJfc$+*OSbrBqjG+CbP*_-zp3h7E2rIGFPel!F9OdD~33%Rhycnm-jO*Y#fhP z=0r#j^b92%xGG_&)fijag9s6z&QIgRkqy%?HM~#}CpVOW#rmD9dH!jsrA0zuU`a}n z@b3YKo$gbAv~DFmF;}%WZ_;EL11xhbRp!~Zj}`E=NnGz8ZKx{J*d54B2Z*uvK!ei7JMM0!wrssXk-vrSiaYO4Dsq;r!&ZInEY#b|*&42Ma; zvDgB{=KRTppxZB)fNtm`)^MWV73&;nVs>Wi{`9;d*9&qqF)Um>41XXu z{DgiHW42#5uZ_cEthlz;!-TNr;hd%aLyU|}aclYW1?wk zX`KiW@pFKTpb%U^uw48R3A%L?P;dSq2w)BL4^9^{W=Pw!+6LKc43j(|kW8Y~iF08( za)4)Iad5~ylW_DY+(?&wDw?r(jynK_rKubr9~AVto-R-l4_bJPPb`r(ToFZ_c2Gyz zPqG9^zF0g@v+K&-%2=^W#>O3}eLsqhNGE~GFR^TkN0O(hV;gdq!b`-J_GWjD&r}c6 zbv->KmQC?{*D^pe31<#KG)?OTF)LD7adcX0j%ZXClhlm-@exeo%w7|efb~)NaG6+T z*q#X*p^op{p+3m_on5tVPEOr9g^LC|c zEUOl+a*uu9VaI3Jew&)k*<#V9#aJrwYe_QO{H;P%SgR}_)YqQy4<7E2LZ))eP%KiM zpMPGf*Tn#>KyR5eUiv^PlSuQ8MG){r;0BzZACmTmPJ^e}RLQD##G ze(FeAGZVQLa{&wq+;}fQawyk5^7^-M+#DrNoOb#YuE4D~xF&H1n;LF+cLqSq5562- zvkq=o{tW!7?x}8Uld;=93cC^YfBL{$%*S{73 zC`j40_0K3FGC9coJ3fC-p>iwE2x16eT4N1ozHsg3C4F$V!sqqokr&9JCUV3T!h9X| zS}`h?_A@RQ@_V7AYEw(Q*^!^zic!4~j~onxU-c}>tiHUf5p#HO)$Ut7luG`_&TrrYVjb&_?-!D?rOhU+@;d!p9H8eq*Tah}tiI z83c9y76ixZ{`R}>N(3}X=kMQ0Adm;W(E`oJ%-p1z;$s3N1>{t}1QGK|SG?=86(IV~ za46{_S-m08$d-XnscL|Da3JCYr~XYS$SdfgySFj$C07`$RF7kW=mFJQ=l0{eT9& zf%RxUpOGY1>EyMLw#P^UPG^;v58y)=CFm1-jsDfIf`xOg6m71*B-#7vx6b2NWjxll z3iS_)Z|km^l=O)r`AeA8J+^Potu@pZTU-_8-O5!lp$gIElNR!$u7AAeQJM!;z?n$}n`gWt$VR8 zs_Q3eYI#{=S$zC>t6GuKU<|^6`pElYkqQ8T>-T~!6rFlGQx~E?+E~bqpLOfzLgjWy z2v!zaU#;g|bv@(Y3|pB_XLmnB?+<)=%xEfv{mI{P?em?2u}@{Mq~Q_dW7x8T1$FnX zPqDc@>KOwqo=_WHKjfzoOOr%k!xYcyW~OYYU(U)D^r(oT7ODCv_lkT#le(5Isc`H8 z6~rTBN=Y~9-~39_4kp!_fAd_iyjCj^)+dgYJ%|<3A7BxNK_y$~ZfcU^SlH`r^1hO3 zkX2xje6u>ShlGN`mz66W%l$HqmozcoS!;=*_ITMkJLiEd4sW`&9dwzSEg8y_!x2bQ z_vtaz^e+!!Xfhfs#22vu0_jajeP|Nr{9&p1c;178Ot&u zxF~EfV-%y9MC-A}8cOGH=bebOKaUmz7=uK=6ChdjV0T|%CF0(9P|2_aaQ#fnP%+iY=xG)rVnm>J>`#IeN|FdNfd?5V!+kmk~INj6C?TA>< zado)7{D3o8{|eP?KAfmB`#+k;F1*}uE671ac&i(52(V#kXYwC0@DLxU{!TEJ@hdB1 z0BO>Z`3h~o*@GUNr;20&qSh+xHYKtpVkJtD7JE(~48bS@?4QG6ecr{&1Y)nVrE|IG zL9!ky7S=bg5+u(Arj&6dhZHczRERtX>Ix$`ETiHiez+4G*uYeQs`ei18x$|ET$-@I z`Iy^BaRYyWyC+f=a8J@_QQBmwf39P%@@F zw0{+lNu?TqDmJt~2AV$1l)i8XrXT=hS(nHCn|CBJDWR;ClFX$P=z@ZLLX$P41bH9t zKjb$)qlET(*IOGU^EBB(EZwgWX9w%O2jk_Wq7+walmfF*RTu)eGi|lm%nLo2X2uD!NbdIFl^Tcl#4!I<$lr%a4=KP-ALxdDK$;)O1|6D6VjL`cXPv|AgTuk6y>l7=X*+5>Uiv0gn&*g3>ca+%QTmctXh1?=0LUk#xZx2Ma0E${ z{d_G3Hj~FoxHAefBC3yMQ86e#@+02T?btSqKlQ4xXc>j{9Vf?gB*@sK)y=||p)@ZK_`ikWjFqV}zees1TKoImU5Za;yHG&PYc1FvZP;LXG#Ffn zU9u}8U;p{}=nwvw{j%|oYVi7NE$6>Ql8PcCQ9ZXat#jnHL+i)hAdLa=*{?8hK@Cw& z#CH)5aw*9*ViG6Ldlp>DBHOjPHh|rLhLni8V64_abTEmR5a>Y_XN4B_Zev1lmvnjf zUKu39#fb1Zmd)IsY|dtwh;1Xf_?Z2gaf*odRZFO|-NwM2D~FJ=XeV6%-iORKgqjg( zl2N2zMo2`5Q^Xtfg~vdfzy#VzX~i?a0A-Hag>ipRD+2Rrl~^@>?={f-Bnl}YfpM2O zaPGLP=%D)kXy5@xHea~OBBc53E#r=g^X-zyFs65}T$_wg5(v<~uC(!m)?w<-f3x7Q zBa&Te3eksO4!B-cr#tJuw$L>?Or5XeCxf(je{OYCc0|*m%WSF>DONWK`Xb5qlSd63 zx6p}-Qn95;L2vL2{Up@dddb|bH{2PHR-+cj#!x)6k{6|FOOrrC(9geQakB4udp0HG zPj2FM+MKCdyfwxp04)MaU$S{G@sWh<(#76+Jm82Mx5w>vK8+%aRe9s_P7-L_16MjX zA(*El-uI;K&Tk5zA}1Visx-5A;f>Gy8~1A9D;@CiK_kch&S}9G)R*B)hquJEmOyr zcxcu6e{&}_YD}{zzvHxvSy?XDSJ-*cnKoI=4bj+76%V)$rE;+WUi~U?10`CdsSGS6 z#P~tT0~-u!@>Wx*$#x$}1Ec@i)hLy?PpWqG!9G;*d0V4NNI5CQu7V*8If3;6zr`0+5LGFa$ed_bFZ_M=3MJ4BAcplt9H=eao&OHnOW^97sFlo89xr6I@?Oz*RZ|!ha~DO#wE6LbVDsp3exPditUGiD3*8U2YHGCd0&CqFAHHC81h5Qc7)wW- zMDIYxy%PbQ@e{MTabMI2$c!?WGVyTWxp>3GoVush3K*@(fB+yrhHW@M3`y`v3U za6L;06KF)7ZVb}Z{n=;=-33vwz1T~&ctiD1>0r!F3lF*O3}YBRm&%YFvBC9w7K=70 zAWx}QwW1pI=<4dG8}n)q$TbH{6OEKCi75Mx8~vspF^e*GIgE4=>wcqnO!#aceu~Vm zalWz&fD&3uO6zf7ZYRA6!`N7LP=eSPw%(NbZataTIRK2DT%-9C6#? z;%E8G>wTRkv&v|&RYb42&G{8t-mLc5@hg!EFhkA+6{|q!>EFLXz@|HN)_>IwwsPo# zX+pinO=TerbrpHct9cd}kl-N{DAd;}&TT#TQWyvJIAPx&E|)>@W(~CcYpkZ{CgEQJgin>l`7sl zH^8q%wR5;zz@cSD8Ca$f003Z101a8-`=KjW>|H8)7{La4CQh}Tn)tn{n2!p zcCcM?J|X!|!VEJ@A}J?Pp<=|0#cs$#*>t73t0IxdE9YqFq59DA}DMNQ#`jUeoylg#VRs_%R> zaX%cV76$6RPB8zB^>1Mb-v}_i*ryp_bi+NSf;`n8jzU9hA$f+zGPHzCt-arB%ww0a z=0kR+O^DiqkFx+F%}ODq*Bezm)?y?Xu#-Efe|b>hRul%@X6aec=w4%y@O+!dXlY5c z7Qy60$ypK?dm0&co!6efoJlQK1uV2%wey$DQYTAQk>xdOeM<0hp2x@tq&C~b1RJv1 zj(-5NU!m6thmb17hY54vQ>2iCm@t#=_JiZ{`qV(UgfE*~5R__+U_(_EJmyxH#ZiS) zD)4=OYv@GdPCNeo%jvM=2k)avt!#^8S63Hs5(iXv`_Jz5L!YABI63uzkb5*(vga=# zCGb7|-xpBVr>rcZ5Qg3tVdA&*G2*;KH3_XsViH~iFrTR!Ca9khceq{d`b8S_U}sMD z{uPgYE~$cyG64TXWZI*F_b7mcI)Z?HFI63y|7}$SsYnEEUzU>7$^O11EJ*IQ%#5Wn zokHg4TLwbHXf4~$$k<$sqI-72dB>Tg3cFIrX!2)g&uqpA^&uAL^GjA!+9IkVU_S+n zNRoqx!mAo!_vXD?BSIn>Eh&_h?ZfppYf3%Q%fo{`lussqf^fAIyufd{f0m zw9T;K5z3!di@=xNJcw(kxwQW3?v6(2jeHN*`_Jxw`PxP^8q=cv*EVm68{(;{GQ&3p zIlv?pK;C1e(wEMA*S5j|;R8W#IBXA~SzbBZM=H$OF;(K7uP{P_xxbHDPDtBY+k&Ic zR`2|d|0#&vao;R0X@s#hy+4}ar?HM1L4-x$a)NfDxQDZ|+4S4y`}=2W)5*NFEGd)8 z-;>pu8p~v>KLCEF+z@|m$lB`QPq-l!%^Z&l<8>I?dPSpJstfuxcKfZc zBgpi|6v>m-FL+yo=yV$vVg2q}?QlX;!kV%dJ zJnnsUdBpN1K^znV+_=Df&9$Oz3PYG(G zly@n{1H3H%EeN}dV?>G&q-@Sv1>2`V??T&YB~Fi=u`=Rg7wgZ*d}AgF-EUBriI zG(s;}X;iE%Bjg#xaECWNcwMZ8t=%M3Q7xN-n&6dIGxfSLEoM14hs8~_%xt7UZq z0!#3sc^-Lq1hfm;4rhFU8j6F4Hc;W_a1ay?B2d4V8K-j2py8RHZE^Cdg=`EKH1M)U z-%82ML@`0P!PX`{Ry4(nS}naUomV>FUHU9W5=|)grBwT+_x^Y_X}Qy;1n|6?I{zg^ zZn`?+c9JSIf)|{)BnZc4ef`HF>Q5eqTN#gMTb#f_g(V%A{aJR@y)hmmH?8wh(~5lx z^Fkae^=?pvBpZv>rO=m})}mj(lnvKzhKI5t%CCj+`m>eLsg-lXOr{ZTN!6=dJHM@X zo3RvPUjIErs^zTss7{$Nb8|Fa?d5q=M=_otMy@_SehEz66SO+-863p6BH+6&zXMyn zf`&LGPHxGPloaTh&s!tD#LDT)7>E5-JyVlAl)Fb>F(}Sy1PT9 zq$Q=hL6DH{ZloIl3BQ~7ocDbHbqogV=hkO*OV35nm) zz{oPa*6Z}qE@FLqe8!_|C4=skg{jW4;|m|F9iZlH_5tj1NQ#9#+y7 zhz;0Fu(lwM{{9L`$XvFS@O9?X?aseqH~ZRll+XX`1+W}T=c#JIw2+U*m5wh%>02KE z(6czhtnF8w^6M)FZKYMi_50_DW)> zYR(i?$olQw44+XB_eXOl*~>KAGe)#$>e<=J61+ARa|x$=xCPSpK;gkOSe$>F#W(Cg z|K8r3C{-lMvtfJz0zJ)Drs*NvjeRgV;3r$7=}PEm|8&a>2OZk&pw)&i{`i#Y9u0aW z>b5}-&0rEwxz&8+-)(^sCw$Mtls$e~xT4nFq7x9ccd}pB81(-1xKP?V(Kk=<9E?Ea zn#P)Dpb>Nh)>}}yKKnu6R=1}r`<8?oslc7uo*0IYC`3X85e{^^$70h+AuS@#P`x{t z>PAYy(*zYCUS3{|D9|=tB+VZ{wI^By&f}9*P`^=N)bL(T6bQmladYG8eMK@|#`p_F zl`0F*7Zxu#G<-8+PZG)J=RD}Pz}gVL2Av+jn44?+3*2p4+NWA1M9^S-CKLf~0Azu1 zwg91=yxq^9CsKkcy1Ev3Sa7aPSyZbWll}K_hqp$ln>^AmxIyO`%$8;BK6Tv{*B4Ck z>W+4Vgk7J-x4f}P7d+0~)^>Nl_+#Ifbrgb6tNO>Qzt!C$S*v95{CWsiQL9dl7Yw|L z!mRvWFQ=SLM4mJNFU)Fl?yESor0=OMlq<4mbsVVv@VAvsH{?$4K&km)?YktZ@}+hN zm-CBrt=z6RI>PWTR&@7eHD}Ryqt&)IT{8UYr$5|!Bd;BPaYD0;8SmS!q_0E?eS+Lb zWoKMbmYsRKxPAlFCD<@DXk5{`GW>r618q%?1s0(wy=#hHci%oG^V>)b8Gl|_Z<3Vm ztJD`9ot;f-HUw@VVm^T~bjf*VA5DuRAr;lT3Q~r}WmTzH67)Us2b`7-0D1-!g{^+L zkQ9EF*R`)4Du2ly9OSPmQ zO@}VqB^veezjktl{<@eSbF|v&{~oOS48uj&G}zUsaCbiRrA$->3PQ=1`8k^&(CK<1 zvj1t6qT+&7>dfwJW&9w0@BIgjO{X5K3_~QpA9PhF2zojs&(U1wf*FPi}njCM$xO9bLXX5q+jgN=QeMq$$O7RgOLV159HEd6JLLzuFqj;*)NIw|oYo_neu^94?A4!S^(* zjke(|e%ETBdl%y_ab2CR7&f%g-kVG+PNkJKL*CMv%|LTsl(G-`NN%HCTl@e>a) z(Pm$w5+K z7S5`vR7^KFv`aZ{dBtM=Py#XskC%=&+OqHY7mneQfr$!r7M0UN3KGcxl(!X}#gGOW zSBG_S({Bo*V|pzTt+V|E0LJoXKaVz2f+$jt)*1tZ>rzH&pL`zgE#2%^bzEP+E0tZq zH?y^?FkV*!Hj;9^5o(rHHc>Omj|r71^*$pPpG-3;<(uB9xH?NaF61TZj1&$Y{)<~X6XNbrGNRfyCCN^od!ShEnEE~6TUKb* zh^c(PD-0IkO_1!W2(L%)+ zBu}QfpPR6VFJVmMBOnR#^lDVr4$^0ribhxbEy!Xtm$aV*8U6ecH7lM!Z}#{tT-FDr zb_tkYzlbod=}*8eUpD@WM`K&!yO!~3^?TCs&Z_Y3EfKNcM8%P(>{O}f&m3pgsM}m!9q%qjO(Ez{e*BY-ZI`KSNN3DWNoheblfeT}!6FqEvoyoI3XhIY3)Gax7IMmv6DGs@66M!Yj~`3R zdTjg)N`mRaWW#~GT8MrCZ%{#C07oJW2-?>4KWd-gO&j28HZ#ZHVEYD?9`?`Sd;+~^ zU-|N|LQCWrhSR$#-dN)-1dV7MgTPRUMlsrgiFl!;!&(eDC`Gt^$>pF&lR@$I`qx?9 zB15_*2m$wMo_j7!&Sh_}mKJO3cl}@EAB3suEDKCOv+so%&zf==wnsFMW|V4_J_y`h zrZO9Mi7+{q2hlKmM%s_*4#%Y61f6490`79ZzZ80Vi%|j|_W8wy=+)k6Xji%7S79a0n^26es_W#SRP?Fv-KzteQFR_I4atD&BQ zn7Nx(de^2vR2Rv#DyloG5Rpz_PSB$ws@j1cT_rP0&nm|}I09HDa$y##@%{vu~>>*BU z3VLwy8UT*XnGlYjKA|G}_11*=Sd?f48hKoS)MbwO)!l_O(bV0;10WpLyJz;=dE$JN zkWr*jS4){*V5=_6J|Fn19JV5MFip>{Nx%ZI-Yjpmw+*OYDwR4zh(%jS%v{R)Kk*|z zYzGB(^S+~wxt0E|J1+5NDER)h<(!1-wtJ&ZAA)#ON`$}gftQklIuLO0Q&pqk4gf8S2(rhZFXVd6!4mW>eQgHW()}F5taInrr z1^$TEdBw5?{G%CL&#%+j6y_Y4!&=daL9$$y0T5&$I_(jhYl2npKbh5BF;ZI`h*KM} z!2v)(Aiy>UMk>7g;jlIgJOD_J)2JKDY&tJsACxSfpPzTzijGTtkGxR*C;>r@*>P(9 zctyXmx_SYaXAGVtvi`rDyubx`N<$3OG)uS10bbX;9b3U>md4;nuz|pTlzkAh6Eoy{42aC2MzNc>=3V;%G+{-23?2;BTVh_jb%$m--$ar< z+6lc*Z%rhFG#9bGil*>z3v;Le-Es&0qT$4+`dEh)m}1&$je^1zKm`;P3v3w}Hm;PQ z(x3uCr5_U{gYDth`%~~Ksyy>0+&$>Ss^-J#f{;=abN3P?)BN_c?m%k2A}b2JTa_*y zp1i4lw3i7I^e^uMlx^P84CL|(^C{1}{mI;=xXLAP?>hg=xQ#%rZXJcs!9HxB@0Z?g z>7r6^K9upQ4TJ18_XWpuh9;YSnGZ>1HP9MOL43ZoBlr#WFV}$|)%d2iuW^Xd^%~$G zZsrZSN*oQTa4x`0;I8vSLmJ3Qq6RlACsHren>C17MHuy$1HoA1Rh(ByT5;XdwQVlxTu!y;sDE%yJ$9$(KIPl zcoCrw=RY>DKslEmKA`49CYe6gVG~T53fB70z_mqnXwwSMOh$qdNXqZR*i{K5fR78F z_B^E)G%4zHHkg-{szqOLg&+m)55+se9$b2v5*|ts>ve?LGUU=JOTv_!!q`rt*46nqe>WOT%9J4S&9TVZW6A3QiS$&qrl=s%Iqhmy~f_CD_5K@ehC9} zLdh5*0X$s7Ekupr{%}`51>EUf8yp_*0cb1oufRH@Z2rmKA#=>aBb{H8HPgM4JDF}x zwr&%%jTCi^Sygo^OQN(>{31t9V!~Rxv3RwdUbJH78$_gv+iZMAHFQB zcOVJ7-&*ls*VAu8Vm!F#>E*kVSwPg@y`}EGTWtYcy?S?NK$>C}_WHNsm{iGIPmF7e zD<(Ek_kp7F`;J#+z$%T?^W(=CFdb-N^XSrO5jjCR|2W<3Yzof1B)|nWdCG~63HAu( z?<2tHhng9Tf&>2Le5eohdl_a~J^=xg=uEgzj*gB1;(&7XC4q)H75Fz1-&l^Nl%cg! z+Z&GgKVC?OLa9kf)?}9g)>Uw+%+U4=c}S$N{k3ji+Oqw5D68&I2~S^tIyugHQS_fzDSEX!XNWLe){V7l!A`e=|;Q?BD-O& z>SY@M*k+3{yF5>T`|X3~jdS-yPE%VOb5YP;;G4}k21+P}8N$(^FpPe1a)*SfS6V32 zU8ERDZe_~8OOgO`2874+QB(oy;!Bbm)~2kh2=8&g)7s*#eoCTVt@DFNCUtzIPTTdB zBOK%CuQEuPZv-cR>2cZiz!WNeRtga;zc&h^?HiYAtPTQ~0N`NlXd%yL1&DQrVA36J^EmLRqNU1qxzJ&=7$;wa(9{p4EqLL;E#xTL zn)|#AF&ajK>30{x{(sPb=OvKYjXLJcoHl#G1?L_W!>&p?H9#7ZA}Rm@9+g1qu1wvC zgrpV51@^w9(d|GLk4crbMV#l~6YxpMss05x zU9p`04A>+Lw0J1l-w?AB)uJee8t@)*n2`I41I>lxgqz`8C#m%!bQY=^qslTyMl41M zgY^j7GVrOSIdT|Xu7Jopy4K_o_^va=x5Kb>Gn8#+_Xi5cON{$CfiZnF&*kc7zr{7T zqKIuFXhIq7;&wjkj@ml-vPMz25z=t1_A|b(z*KtH2HudKE-}y^GbSyU>PxQOX0Oq! zPGZxb)<8Ln+b_uslNF+{2^9siK%3^|U{xs3#CxBE)0(n@0SpjG_>WJc+UhS=tK z;v0rI!{z|DgK5;8@}@fd<;Fbwt39#egM}j05~sq?7oBS#3X))0qDqg|TXc9jdT092 z=0-CCm9<4G=ozhbIp;5NIkctJIf@&zb7(m(EnkkZREtkeJZTz%ML1KG|ll(4jeyy2F4efTd zz`~if0B9m&qXTy@Rc0+zJsEp zQTs=?o&9;iVG4Xg-dbwa{$$r}MC*6AL%#!-Znx7FB3hG(S`glO3GC&s&>e(( zLqLZ&5)dK*NRRGI4|U1%Gc73SQcuyjehokbsM;?`Nu)p^^K1?F1dFxvH`qr5S}h%k zK21hlu~PrJXfXGu`>svszA)p$fwh+2pvd15wKIUi76M^Xk>a+vb;w)@F*lC^Z05br ztdul!li)5vT~3GMn#)j*AMGyk$!9-q#3p47lRoMDDX?9PnO(gLZ44p zpi`X+i{8P#@MgP#f3pHFB?|=W&%eCk6_wz=u}j|QG5gFIIRyq3M)fC!&O>uBJ92_5 z5#v+Y>YcZNI=y+-A`m_klhn%#>KGfmC%8_)1B;v|&`qhFY`(B5mk3LbB}u&n@%B*G z=ZAmK*9>%}GWl=(-@aJ!PTzeLaY0WWyubV{9S0J{ii0^I7HheJOb^%;OKJddRB*qBblO#m&rV)@Yf-E zhMlWqonTPBn;=E~ItdQS8ndU$S33am(Jck$Ni5ucVE(O5j4)516AL(V2FjQuSVUDo z6>oG1sLV!}eA+CK>My$M(J0qI2e#EYwr9|(t$?jeO7LE!V~D^o_~LX`P(o7D7+fb_ zQoRx4re&?;9 z!K7hy6eve0hm;t)dlfy-B|LsSDnm}~tP-LUxjAt#or{ueAO(-iEE`d~r=+CS;@9Dj ziI%^mFo+omEwr*4s!(IHDp2p{fxmTR%h2SoA~LU(-TengC+0^#EW?LG=AkOO&L0D+ z{00BImSPfDhHvr9YHMt&^!lUZYIy)IE-EUpp(3*!+=&53>MyXVP%+aZfbr4PgJ0tB+hqTlGNuhWy#ilgbt+0*e!X)Y-YOtFFC^aB z?1(cb!5EYRA_yR|09^dhUq$)zu(2tPsq)gk(Tcq}X!U#gL=(nl`Wg!c9tAG|AYcw> zN=<7z^bMLoZBZY#Xk|AN!+O>4UtfX&r36{}76d}}r;4KU%_xUo@+sG zzlo`-sjDr=$*UJsG9)OO;FjvGg99T=s2Tn|av`e7nsFkseXNk^?nSE3h$9jC>VGde z_$$-%6t3u|uvB_upuzUs$+@Qpm0(6N^6hJ|o-TAcepU4~EkDPP`5wj}%6Awe^vH@7vv;IqZ@>F-l%CA{mQJ3Fg?~}1|SS-OlUGHG|A(T8=v0@Hi#kO>ufCZ3DCH!v8(15=Mzw5rxZRSMV-{>$U z;hN>v?JaP2(rDrT?_vEFla2%a6-6?G&8s4b@7c7dKp&=Bf2%-k*zAr9;QM$)`BXmo zfJtyu2Ei^i*xua^PMA>EpBrej2Zj{*WqB*Eve{~QDO1rCTb!uSE@WWyuCX~#Wy@nW zz0b@S%jl@*j>L>$ti?_p{x0NYbqb(TcPc^?h+~oggJTw0s>xt8?w!u8t@@AfBK9j>1Z>bx_yib zy+UCzmStU?KCy81GbZ(A%%V!S*Ls2|lr zAm{A?P=||6RS6ulkER|cIG2Ga-P;!zyy9k92mNT;GoXS*Oz04xpCxkGbf=~nK7=^+ zo5{eA|GCv)Zxk+JmgQ|(={(lKfv4~s^^*D4PAiISW( za$M773Hdm5c2IpsW#d8^fu>wC8QtE@#GMj$|F`xU<)`1|7@Ga)@vMbHMj>NZByrh( zu$VY~}d_X{bY6^fL8=Bh9KPK^khT25GJaL?h&gNn{;Ece}}; z^XEb14qu-^|HaoBn(O!7o_eXT%|m=YyE^9qHb8667bj-t2D{}nUd|42l;EGoP-kzv zs<3%z?a$tM?B&ZgoFO`SBj9gH$6|1iI)Zx?yY$1XPOaH~Bn{L8TI( zQ=+gPW*}%~XYP6>{*j_|(8zmUUWyjmNHVuL8g;n`I*C}ufpUmSQ4mILAeQDIziYkV zorQ&=p`lK$sUiEMNaDF0n|_9{?<3=3lwP@d!D={$_Q@_w5>rVgHm`yX);~6kZcs z)5IM@FowWbt$e6|s$$GqS0I)r>AY?eJz< zm-@7LXg*>EmmDPqJl58;e{WhfB9B6YEAM01T2osa9mjF-WjKFV8GZveB$EqETP5Mg zOVrqlD^u(FfQ|!MGr)y-b8|c+OV|r8OOs30oW}15NJY|!Y|u#U6@=~0Gs0a0h%ZmC}OExd5);70d*@)n*4DJaBpx~t!ovXrpM-l?<= z4dn~KRUpvA*c29wLz=!&CTj9s>vZM%O)xEfnc(F1yoT9xvT3LBiB4}1pTUF#D=<&X zVJdZ$JxG|m;dy&8%i>a{7CteqJu|FXQBpz~K}=AyBWgmunLED3JM%Mq?z>JA6IzLP z?&lUpegxSznc}|1=5ozakaElTL&NYy0V9~7U*g#0VE2SDDU4!YSjbNS)5D_4{M5r= zp%{))FTg#+oXSgjQGOBVAve0ZvX3l&rrHsSb^|_mfHrE=n7TzMZ6!24yqaC)i-HtH ziFagPSFDi7KV4kY68?I6FI$kwGv}S1aP}xR&Xv`)OW>x^tA6MnlDA9QFq!=Gq^W9F zEr`_b!w)3Qps2jBC-Opm=awku@F$1Gld&8Q?E93E1uev+OQPF^TkWm~@p$~77ocRl zqr_1+9kUiO;v}Cw5-lHT_&zQufJxi2KG)6D^V{v&y5O+Ku5dc`U>X}qEYX{=GR-P> zANC?1?stMeV@ZB4!N5z31AZwarF8VPNd7aPd$OVblll>FSH@?C59B(vX2_fVmDa#T zlMAq=Q6K{&{I+RNcxUM^^-EDkRBSAd92$;22UR56UxW7t`qa5Z(2yv1hv~3g8UoQ= zu~yKnoUxsVE1PKQ2);F++#YyQNkgOXR0=8OKeLT&ec)gSt+o6%9~s$uI$iYP!~E%b zQ_FKoNld9;8J_&v0Z2nkVTE;$_8Mg#K&fwb2lO>#Fi9?P(w@K~Y9`=}rU?oDoa262 z0LTb;n^he(4hx8J5GgEmLdmP~o3i5C6qwFMfKD6nQ>!SHn!liX6WhYo9N zc!YRnQgALMG?W2-KW%o0u}^pN5s13$G{0l%T=CWV-amd3!4kprQkhO3Jih<|*@Z{c zzMj`Pk4EHpQN;tloy>LK>CsHsC6-%6N6eu9Vi?7`rcm&@ET~b z7G4sOGy%6+z_y|ijzRWP2bVH1%sdXZf&x$vSFKNrhkA3osAm0z%EoJqL$@&o(0?WM zpLo_sM-^3Ksx@>YcSxXRO+m$}+tD0bqyu!)aSs%y z!TBD$r~3L2E5&#e5wCB`HDP|ur~6@^}4?!~8Mg0+lCo6DbmcKSlV1?&cT;b8DaR5Z$l z>_5*sSMCe$fGUWz`u7CYYRXLc1_NRy#$ouUxwoX1qDe?ov zgqZ;>#(R?~2K=^WFXZy$VQwS3IxuoPpSHkK1N1d}DNI8WH%W zriLU(6G8|l@^^vgK<)6H5#2Q(2#c8X?Hfb%dld$->g5BONfmSgjWm^7y>scbr>=Hb z6<{_k)assbl^Jya1wrU?E-^1Nen1|i+(a_1Pd&r{;HZ{-_px-x8BqI--;6kGYUJgY zS*~~T*1b0kygpv}(kLZS{o~`!n$WOjE>tuJ21A04gK|f!(oNP2LUp~rdPdyR)ZT;?99SX%tDHKrwu-@4|SR=Td7oNlh&1 z(|mr(p2{-)9Tsu_XT5c(FOh3NBaRhEUU*={WWFNFuD0ILY^rLJQsvLr7`s;@3TO@X zODX0b*1#hGhRVOo>(2vQ<7wGW^vO5Wui^eaOgCfGgc!r|55DJAe%oII)TT+WC5LSP zSpNr%9!Tr@adrT+miH3Jb;S01x_)e7J;at$6{z-v1KMytWR9d3>}o7Sptvm15qmI; zzb+}ualNx+ay_0-CHPQOODlcKA#h}Z*AxiQy(e^WWP(zCL;xu9CzZ=oN~gvI{uMJM z!nVBEPlOWs55NNx2ul-SNC{NS!ks^epc0E)9P$@hym?8T(!Hckjp3B}b!jb?8XIwv zEq*BzK7dE71X4@8YMi1h;Qah9)zujrbr`0OA%<8|9-q}n`v4ddSY;jJjFi93w%KZ? z36BIz$K(>BA+nJ^yq-tYDHgG~JuX$-Q+jfES0+^O=Qz**ahsyIvw6pV-hN5KUeE(C zPC7am@%B_@7WipPn*%tph>2v`mK$eluQgp0_DK)7#S;+mc}Nk0^aAJjz|Rv<=xgig zxjDR5$Y?WPF3mDu_4#SKpFMhYwDWfQ1EfX*89?%$=}5f+ST2i`U^)ma?KU1DG&=RFH(z7<1< z+nHQ)Kf=j@wnBz=34Oa;JOwf@Q-Ng~N2Q^^0j6<%${#``A02Id>L=K*y6NZWj_ z;d>ltdCCAAXRLMfMbJ-e1(M%sNaEj(rI9vnw*YFqqcL>`YF zR9X{${P@w@XAY6izz!|cd6Grbc0*ORuBH^7!S|5G~`|PTq=U{`I z^`^~U;hocd1TWkdpuMt~@pe(!&P`C#?;_F4Brw*%>w+2w;C<8JO96gZmHoZZ5IGI+ zdeBspsJYz8`uZE})1wtW?g9J{6bO5sjsA~{n@sT)zs}AOSR*!sq-jtxrY)@Ve7B7W zgN^zFMMeHA6|9hq0xju|Jgztto4yF=J*ds=Cw<~4No_%eg*_2g8G;E*=Ie1pOj?&o zk0M@^ZaiL+Oa7=uqe6OGdjGXy6-Ca*Dh% z1sy6OKfql;#0735tQeHJBZzX{kg5jfzr$008#NhXNLBI%?9i{b%@Vc(jwe5 zkP*zscW}7AT5hs1jW+P26bPj4GyeMCH=k7Da#7}y5}H<#6j~-u{8QElXx@_9{-(6+ z!tk^J4MVBjI$tUti8hNx*_&%0)9`gXEhsyAFyQwZScd-Cj05c<8ByGp7P9XcA4xTD zGs_BJ#eM{mhDjo3SWg`^*j~5&sA3QFLg;0Js{(FD>aK4X4306G(N7gKna^+m)%A^K zt9b0LO&)?IZbBNMu^uSjr7xj@dTO6#UK>z2!r}o8^BO6;1`PBl_llo|g;u@p5}t(P za>3y)kCDOm@m!QqN&#V#CRuVicwe%ELo+($4M6p@A7arJl%kqxNClMCVX6W_3{(UHNCgX1p)cpU}FiB_Wg%@22nH7&MikS0 z1TppVf!Lk3iZ3PQReKy+LYc*{2lIkU+V}-@K0CHn-y98n)8t6={gC-4JS>j5e`{ne z8)(}RofotS)Z^CkonK!U5^xx#H}T^ZMB_1uWpXn@gEVtAIJyi>9da?X11{G~QSjoQ zfs_5!Hcvbp95Emv8PO#>{bYL5CWlT46hV1yR-7YlLk&KMJd8tJ;G#wyaM()B&fcp& zC`(mKB+#9u< z{{wnkxIxx2YOz6{kvo2G)cW9X7Qa&BN8VwKY%=#vkEFjXWNhoU-*~?!ZrGpphXnd; zcJn|3A#;eNsdLEvZWNxe^=)EEKY)Rhfaqu(RP0JH3LNFP)lzrcC_!JHeyeG5kqWff z7}|OnC;S&4xhfln&MIbmZf)qTeF?WS5$$=K`|+Td9YUmAPZFJ8p7to_OhGO+oegD9 zg;tx!Ol;YG*6eW^K7q-R34SSJmIBOcdY%d zti-`Zzy1H-C0Zzb5gWm0!!c+D=suLhJfHrk-p(Y!fWKcfww#hE^p;zq@m2oy1Z$fH z*KvG(kDCHNApn^#PjaJ=AtAiQhSfe3R{l?6B!u^ zckd(4>%1iq6&YD0r{|)wEDmL+NlOFTK4pp6w6M(BagOFH3(V2h5r*co`CaLG99Fd4 z8ULOAdwTz}(c3Rni{50VAb|dz1Zo-@HDZg|e{sLve0<))!@*5}kIbTukCwZdi!(kP zAih}v>?4Bjmrod_+IoNll}r0m297nNJ20KlW`uS$?vR8wHnY*rx`&K!O%*^c>DJde z7J27dyghYV9LO@?%k$Yx7b26z#J(~>QLR`uHC=p=DMv?(d;ljm)R9Lq9s9mCw$@us zjW&+TZA#tP=TXu~QioU2Sc(btB6R5y>Ln1_((2(Hn(zpcS=Sema)Nk{kZ;*1rOQBI zI4+{1k+#XOwZhTS-8dtt6p*L{U4d?Qzlg#WJtDSD$9x_;UBO+!>Y>NDq$GTW=X)ae zZxJJq0gfsQXJo>($}57KvCSG13l6phJ%$;;hP_qqFimf>Ttma+gL%4Ipp(jsjf74e z<_KNiVKq}289|bnj%xkqtHU~e6ntgox%b&jWo80JLvqW|gv~sUrC#Il;;SGN8cfVG z?WXTjB!tYmRz1dNVGpKYKoxK~&pp?0+4&{=+3#=UMB}73TA|6;93k!VU^&`|)`K@@ zMxj+saiqQLT*_?xA2);8&y*=ic+1qIcLON=Kcj-acR8SSOL8#sI&t^;{syA2+szam z@%C?qb|Knri?OdCD6bDHdud23+$gaphyuX@h-KVMJS=Aq9PH-#VGh>FvAR1zw@1>> z(UE*AvwH2pZ4X!hSWY-{F}lqG1~wPaLREoOq}~EttaMn$>`^Qieu4sP)WN<Spul$yBjrqnWp8l{sXN*bEmP|fRx|r|1TLbwWYPd@_`Ox-pf!*xzP0+(-$*`g zksV$vH(fV%Jt_Ve^>kIK>whB#1Z5w{}dhYgb1aEh#oi4~wamoH@*kVHj9i~E29XDO#)4IU3zN^581o}9fiQEFTX!vvj-G#FtaE{uXgAr*6(uG zGX?pyay!FKjP+Maq2mwfNd(Wa@Bg#qV2l2N?dE_fGrYOFGghhFE8=$brx>{5_ooQ- z`1r`ka@|$Vhf{l5C@&ti`Vc*eh}&H^SLJ7X!qL@e8%|c0XOBT)8otER)msNGyf{Fg zP;IjqWo@OOH?j76OnleomRAhsy=(;Z<5S@p1_2$a!f0X$oQTo)<=ns$mbMIkAf`*2 z9|L4CZk#SAHImL~4DoA;^a@nTB%yFj)Uyj+#M`qqIv?9W2JkJbty8MJ**2r|XF%Rq_nsTZa zXgmrg=G*4Z(%%Ug0iYY~f9^nHNP2QIxu-cfc0zS6MQdzu!HR6#?=3MDiQdMs75l*= zPWd26LNx|J1h&fbegwW}-c&v@mks82jaSOnS~A_IqOzA|O_~c@aVUMf=V=m=A+Ixl z%RW!QI$W>&hV}05jt}h;jfQGSS;R^=sL!Pyamqx7o?W{kO25_d?~h1s|2+jZDqkMN zkc2_~wiISX>n$Y8mZ=6`Z{E32ZA#fzpdb~F=%aO9Jd}>m#|GPPGV;+G4)L^qYJCFr8AzU5Rg<7h6Vc<1gYIiPhN?+iJSQw*M^#3!%RKx}o zu9z*985T=Ex+!-N$Sl(C>53NskkE;shiDjIH>rSab8~042(``dKv@VzdGr3@-u2tV zvU+?1>45^$JFoKq6g);FrxE+5dX=PvsVtJfcONe&Knxyf>imzHv9WQz)g*Oe0&sxH z0atm!6jS|)&);jNSaP8Z2h_$+d6akHe{x%nk-E6Mn=ePxk6=p<*Nyhf8$Ou^B!4WDGia&6OLz4~805 z7Bb-yP%vEPeYPeyZhh)EJP90}Lk23#{#$VBl`7&hu|xONHTfr!E2%6YWmHNJ_b0l$ zP&X5uzP%2j!a7~nxVvMSnhT%n1Os-+bzl?-d7=_FwyI^{SWM(@_MIr5gqxm??FUz` zs+B?99uCxcA0q3t4}#zizzEyB{9U;;DQRQJ=UKngE3wt{SjI~!nh?o~0SBkVYbc4@ zp+flf>f48zd=;9g_ud+O_*e+u*9SDmKlKnD*W2?1zJF)!;&+|jBmZZ*R1X2ooRSA9 za_F!0^^u3fbnDtYPn9d>QjxkSRQj&m_a5)_9gZD32is1XGSSzX6PG+se}(l>07m6~ zof%bqARZT^HaK<8?>^`iu4WD9?5z9~&jm;%H@V^6k*^@3C<-Mz4_?J25(V0I7T2JU zCo%<>Cg3a#eA6=7Kv7mLB|kl0jnCAY%kwtKXWfbse*NCcvQm;^v zM1EHb-mhscZrA%NMcGg(`Xaokd+kVO<2l(N0BMf}r8Z8*6Y+v6OL@VRg<=tI|CSyh z;IPm}50^ipp)QQcfyw|UtC?@QZyUp%KnyQ+E%#44oV0FOF*cex1z2s(64zwv z;7Ww0fuC#*S7b1RwT4aWMZGbR$uj+5wYn1%1<{jmYCp<%hwTh)L4!vGB^X#9h^k|N zm$QKETZM~TD%)8d8? zAdQN(#JrFR^QtpG4+)Eu^p^*`zMh^H&|%uv|#xZG9~lx=+! z7ykE`lBK=U(UD-xtJ6n37<|S87$I0PKBsP@jh6@71JsZH_lr5#A(c5KgezRsLV%Mo z04>EMfl4$ol4d+@`er2<4SzO-o3%2lNi-e z_sMrA0l!Tri1#Ts^{v@h#{0)}Sp5!v!_PMFXtzipRqR>6ChN$MgfjpDT%7NC)~S_sW!bW5yl@!$>rTh|>%ykoM-b z>z{s8!wjF4?dy6*Tplb!Wzv9QU&}Y_HxyWexj?!&Bjz!NiOqheT}4-D7#W~XWmJaK2*AqM;7hh`DIP5iGY|lum{8zA!L~>F zx!2QQ_t1zy38wJC5F#ur5%n?%*FMGxEf911#bSQK46X*iDOVeb#+_GgZG)D+df@04 zaM_7C_iR+>r6e#HrF1`>aXVh%3hpeVhDH4`6)DmE7fEDouE0R()8#(4JZHieaxMqb zXbwxaAo0`UnxoliBkxNoW~2+M&w70q_hI!oUf~Z4c^SNbX(3u{VzX>Y{1U0F;3aQ` z$RM=v?nPstss_!=+X~>OtKaUsd|H0f0EJ!e!;enEr#v!K1#D70U4Z~; z?}hzx+h^j|Hq?ijMp)nqu|ny4s|b)Oui$BIN!An}JirBHX=KTpt5LYqTV(BYsgB_l zBV=&Pxbx$aj!NMcBNm~64W8n|t>eTZS`=coPLl;yO)!$6?WP#73u>pS6egJ*z;WPo zvTheEs2~}x&@an5(b#mpFJVFm4Lh9T{qbnEgBBTZgB`Hr4a6061l5jD?tj4nx{{(W zrW?#4#)g$TJd(({B7ra-r3g?HxiFkyPZx0ALn8^ZYJvG0HZuSY@bbEicE-BpHnU}T zgoJ~8#n4W>bl_1pQ(jn4u!mVNU66uwPz^vx~%S9 zRNgX;NwY*o0av8_U4E*B76dP~N{g$X=foW9ufylJm)N$aY$ZOS}QN~`& z$(s$ZBK7iIz$H^uUTb#W4l5DN%#OFj9TcV+h$|xZ{i_8*hV3Fq;1?yuYuJB!$gZvY zaX}+pVt9Lc4KNleNU8X5vuu1QP3Fs0CGUh#Zigbs1WTN!i+jBv=R_BKLk~8e9*+Hr z#BY#*=~Cz3Pt=?6g=PbV%{&k$Pv`L5l4Qtr={|tTA>3q7{CJmhYFK2mV78#d*x+g7s=yt=O z{ryCGGeCV+O!+@04jK;O_DFhPd)%0hTfp@Ir`(4|TJO&mZ1_rvhBS572({-a?XAUV z^NMyqrY1ZMMlIze&|`U2TK>@nBpT(sbEQWI3=Zb>qXw&)V#S$}5&4Cu_y6uc6!k{w zY6voMGR}%95l0$g_uXU)dw!2V4R%#gkHqKJVbwfciIPM8baiN~oNB$jDGFfMxE}GH z^H(r1#~BfHXfOq^ddXoxxuxi42!&hpqGhF<47s3QEMZ~n!W&PPPG1Q<;kQlY!pUr4 zR?i2*92*4Zw!}vh%N$Z+V;`4MQ5L4M=q==_Yf98K{qSL2lRR0o zk1X0D#({1V*#H5G6b?h-;F{Lf*5SeeeLcxF%8P8}QdK%off0#A=O$FJ8@p27lxY?D z&Iro)@h;K61!VnHz}DS)NkB?kQG(&5ok%T-2NNblAWUz3u{%N#6&0oW=L-1p*&_-m z1jV3!CsWoEzJ8g)bOC_of3GH#${ju*#csI58*Jv|G`{Ofm#Ti-bNcNjEW!v~6EdQ_ zTVp(eFE%xwa{^2X4aN!GRjM7W`ARDFUe2o<&7ie@7Qm1Aus8NL^B{){-qe)*6A7kd zZ+-Xz)2VGh%^fYI%$Zz?KK|f$ljEyic#M0=ihmnD+>RF}*aYjN@Ns;FGje!Z?I!Ry zChg-sBu0RzI_!&9NUjphY74|uiPgb-e`=tdLTw&CmurS-Ku?j91hNots4613=hh!g zsTe6^ZTe0wq77}kNJdq!kXW*5i2#vLiwdZ=ebk>ZZdm@Aj~qXXxE$?`&eW+f7{^Rf z+u3WSk2a|Gyb<_xq=BMWA2G%yw}}E<92|^B^kynO5_iZF(COR#PjKvqwqj&5GV1tn z@{Zoxy9`G;Fy@6+8aiU=A^hC?l^U(2-#+-asfq4gRy)y}yrBD$6braXk~X^#ul0mq zpK&DrKcdbuEUUfS`lNJscQ?}A-Q1)g-O_@TbR%67(p@5rgmi z_jT>;m6G)dD$uLA(d$^M!U?Ayy8l|-}4pF!_Gf- zs}cW16IRr!(bH;l_k_u=EUXwQ^!YTsQv^^AZrpgWo0X8g47bvPAXfgp*W*wkF4e+! zn9!g_6oP4taxK@;;#G4rB zr87f+wy)gOZH1wDzcR#=BYh3JHnCMLDUHr{Ne~izfe(TR+3i=`JMgsd?swe?JLmFo zzj@9vn|;u5*w$T@#x9O1EtS$pvNnd3qM`YcRBLBEyL;61DG*{=yIW~-w=k(y@bTe# z_M3(L{Zx?QxrpCw!V*{VRnXLsxQ-8~La_sh&#XmWMpvNEWE#YMEUYE7{!_m9mu;{7 zBI=t49R2coHAZH=Bly?!779er%k-c&s{zlD%S+a)!cYwM3@B8EBADS0UU0<}or`k~Iic621Jn0*y$Y$+| zQLowU-hu7vn{nLn)R(Y$1jAV#uao}8RD>Z7mSpI1H8;M<+*i@u~6l>IxB&wNE7 zlgA>Q{Y?zedo=&vKOQF6{_C}w&?sis34L0~3VGS0Uyum@fH$5e)MYqsi@NwM{6jM%7@qedjmyC1apz=@^Y66|2yk`UL`a>QP@faPl=vj;m1~8a zh!{#aJs@tpX#$A}FSqV6WW2hk!0}Hm*T;(usej%O(EcG4_QV{b5cR#LZK&O-W700s zcrENK?TCodkh%a=^{~)TMrM3crEf-`mBsI=10R&L6NBYnUe}t6vj6+KCd29)C!O;s ztJ|)n@V^Z@74eljoG#sW*@uX&Jw3h#Q;t?kk~k6%Eh_<=Nlm3+I~@n+&gb8{MOP}elpZ<=6(F)JiZ1lC^yH}73u2K(2N^#Qxo<%z-;vVKp>_d*T#@wd$M|-HRyW> z0!u=hg7*m`$-F;ZhT*%_%K3dYFEcr?VN#;K;>dcM?d3ZLW=`R}pSG!`vuhevv`_dC?0p_dD+d&~7S$8)gL7 zS51TWR!X^@jzzDo;*TAov|1wK0xk^bxyUb&gWv;K0XyA>$! z@h2JOG*+qxq7CMi#$wSCeN+Hg{+p3Ae0eMb12vn8?pP!>a_mLP}OB8C0TKrnO zX43Jr7uEtUu@Y>1N36dW4VPg7go#h=$!ly*tB6RGjYG zU_KxZMf=kHaax=UM3twu`VD$tm~9ZxL3R8Zj~6JN%2 z8Eq8cNC|^V0p#vPvr{ePaWuLAGndTk#?3(yk&@|UJtG$L6WATk`ehcB!D$B5KQK^t z!l9lcGTZL$UX0cUDw8I*2&zRTJ_T-t&@6#QVI_di6oMf^+uXc>#!2&Z7zzdyW0e5Y zBvIc0_^6|O{_lXEM*{<{g;WZGg%@(OF1BLU)3n6G-gRXxrCI+j`cdpR_vFf+w>aBL z)xcr6+sGb82`I(OSe0^fN^n+W7&?FVT6s_w7Az@y~=H4uwNSNhoF+f~{?x!XV_gfO@LX zDUF*BhuJBnm?^PR+=62>QR_uOhmggT2enWX?8enErnqho1x%2F=E2X;p@@qMvb(lFYlk(IfF+)2m{msb|FB2zUIb*N17OVz^xm%7%ea zeT7iv(nwWi#poH?_50UXrkKPg3nwS1=*KpmL3yYXEKqc+a(a4-t~&2`d5~Bz61b&_ zF5PzU{&0HgO-Ylp!u7-#;q(gR98I@lRdovSe~OdYvXlE9nj4b}z zIm-WgTK`u~${252HW8k}T2b4gqKGY;I6zUHl@Btv%j>0q=N)1_50IxYQ1L zVM*|jylMBY_#mPA+$ysgU!s|Hu(J3qUX5pR%O+1l7j?P@gr`*Jyqb5fj>bEDk-0|m z8j)#(ToagMF`%8OVbLA8Yu?g54ix>N3KaXD&`7}5o(_mkD$F0}D>$ukMvBH|2UTb& z?hE_b-+o^K3|>}8i3N+T)uds-u;Cq1vCS7_q<2kWuu zaZEZ2lI|{$^hP#6yara;)#1`6uj=cwRpk>xAKvX}!cGsCIUAvUDBYHLejtobDrwBV z4E_GOe*aF%B=4LMq)`M#DnC#N$tYwi0C{vRDjxz6XpkI94y~P93&@zlbPf}x1yWMz z6&ly-j~_oW)x$PTsruhZVCQmKygFHJ`$hlN9`vu+fe7NzqOV-Ak_7wb*98kuc!Ic| zwETWw`vwPRg5gmPf=p3tv)U85%g!)|azuqs_@rK?N26XK0AA7^ZtrKd;tYeD<)SQT z5kH)$w>id+V(nh6#etm6zIQcKJf(MZ2N&wuMm-LawZ^7N0GlR%NM~Q7XUOVTwCsw( zdJe~*04Zk#Z;$jMLNR^pg(~3x2OidMieTo2R33sXSJr{aAD(8WlvhNY67DS;^)5`h zD(*>8a2e4xHJQKkgt02H;One)gjaT20H@P1E+_t|&u2@=;Dbrz30$jy(qlOp`jghb zu30${8&f3zL zxNEA*;9tRO@;f_g@|%o{yI;`q))>E8U`R4SAcCZa!eYIDDT83v$%E>6xU->B7r5k1)NaMli;I@kb2Kt30&rJ>^Wx79 z!UZoPLBT7!4DT3%nD2>y>0n;chfNSEqx69-EhG|r>%+-W@ZlB994TH>R$ipFG9a;_ ztEa#snSyd3o{#tMv)VWXNCFMvQn*xzmO6ka6Tu8AGj;d9<)cg}2QeM$ji_Z}QYY{y zbe_ixR4Pl&-jtSsN~4)TR3nn1obBd8R^Ei-to5I1%Q zmPca>sHtbwmCR^&e$#j!Q$lWpGeJpJGV|-9v0vpTxt?SqLon*#OFo+~i(+Qdaeq}P z7=yp)h+)MG^>V)lHJ)by|JtY*hnXrJS^ZZS{&LY$T5}jFboPdq2Muo!LiDRKkf(Wm zcsyK3!$i@gpsA6HDuJ@oU%30t$siz6l4c;*;t$=IM+zA5VDvOQx_=LidHaK@oq*M( z{qq2Ka}W|TS~2+eXYQO(QLL}dB(vv;bF@1ummeY`j2xSO%FT5%mPJEQ-*;Z2WOQ~V zPUqJDAM%;W##Z-Fm~$bJco6mG_kmH9{oiGB$q(VmciCo|n7I-Ql3IAxeb`&usR02O zw_H#6yC}WL?d%f75?TfjDT*l zeOONz&obbWaG-2J#BRKIa%y0s4}a<)Jz_ldIEoA}9Yffx<ic{ z^#>$9>S-akxheHPGPkJ%oj-$}P8;x&hnVHa&OmZtwvW(he2n_SqzNSD;ck6i{6TEhD`6U)EG6#34J)L5knLvMmpTiKAc9Pz)WK^$%*Uh zN76f_90t#idyfY$7wryJzY(|%Ve#{c5>R02jHb~gXv&~aF+vp%~A{KxjH>`Vs?`j z2?7oy%Cdk!Qh0a-&0QFFqvpXr4Om!&+`Zd~Y&OI5P_8oJ9iR_WbuGlCd=(VPASNNg zFKNib;bkP-cstwf;h?E!GVZ+C!j-R@6G`RG?KnZ&(~8s#2GkNY72h>NJXyA-hD~(M z`(#7~xNJgb?T+Dx%R^~ETD~`bPwM-dk|c%QI1|+4rtS#pEdx%804F-z)C)seilp<; zzs>bO|EQv3Zf+VRr*sU*9o|&HLp|ft;aKE!M|2qBDZuWnMy@V8(L6U zc(^?n&(}&ogo^>Afr-Se1!wzW*6{c*+dGK_O{!0gG-_tc64YyV*Ih}jsz5qD(k-s~b~9F12oj3XzcCbrpTo$&c($R7a2Jy%-e(qmtoR5?anZWtyz14~i_5cEK_g3U1!LOO4Sh{1IO|-$y9{ z_u%G;lV2Zz``ss@ncw_slj$*DhWk>eT&p8d%^quuXP2aI68sCL4AOVA(8>~ zHQ*t`-$<8f3z%2(Q<+8{940for=$nM`hp}w9H}OHX0_dm^n;rxgFa8SzYZ*&TST)+ ze?&ig`tvZExlmzDBGU>&QfJg?v}*02Oi9m|zuRC~O(gYa+cCwBCvz3LKqDs6+5XVh zMab9#31GGw>zz!eG{w+f&I`aQHCOS5zc~NmhcakRz=oa!?%{tq)1~UpF73Iw6lCK5 zMvWYTlm3`puj#;bX%-OP6>pl^i&eAI*t{Sc;F5*{R6#vNG@{qmMHM=gNgxBp;S)%V z0{lY+N-j~%@!w^T5Ijct;lmBCu|E)p?O?Uqy<820=M#utmk10_!)|SD*#g5zsD3`_ zR8Oek1%QL>z@78`?fK@9Ss_-kuSV(taOYW9H#ht(2`|Xc(ElAQ8Q|#S$W@{L?$>)o zT+;c>@4ue@`^gFm!994y>ao|3OFmu;Eu4HXo6GfHO2?vuS?2%{o>ETkAm?1+M2~+` zT)El;k{%REy>vz#A>#|E`L?RnxHlrsVmL3D}rR=?Qo--X(ZqG6(!P|YykXGw1% zAu+n6ozQA*mWHuv*GR!Hk&}%2Cpf&r=qdv(bgUHc>Ma4F|#@+`O`QX3O(gjhbDK;)h;c2$k zPTe)(A=!SyRzcQfdONW-S& zbID*#+*}rr;Oldq9UP1Rs$>7O8Z?|8qo$FCh%Z(*aKytzA9oVZaX`NMf(Y!=%o22 zE<_DWS{Vii1ECO10c@OvpX(nU1$@6cV7dW?@}S-2laXUavc~%Ts}R8v84b@h zf2_Y7&u}&GOH%y;aEO)5thKY!ii!GkZVQcN@WooIW>EVnvX(>9l2#!hXn;VA#)@-n zTMN&NpCPJWv6m~ zfnDvWwp9k^Cg9zs+tA62?0FsPdJq~zx}WZmn)#~?O3_c|B+ZFKZ5SC`8~C8_kWW@e zQdkC~^WVRaB?9~~U*C3UF|VT$75#3`^w!;4v0WNG{d0oQ4-3Yz3eCdsik((L;ca?u zT9Y;q*6EAb@Hp>TQy|%kabXCd{Of-`U0TS-`!AAE$ z6CTencVWd9Rx@sqxI@9my|&^&&+~f)&!hP;DE=%7Fe=0%9Vh85!G(szZK@RzXXgN@FQxa7o2{9lc=X)Ug}A3u_b2fS4fg=Lo4 z$ynOEBoolkjC8?=MJkdllw;k;j?LuB_t@O5Le`{QbXq$W`!5R+SVl6QU`ZnCN*!yX zYuR@OcValHyM-xjruZZmYYVbkL!H@Q#OpwrKon?NOpqY>ZqgXUj8fmRkAK7LB9eRx zX#PsVX$rMFmXZ9?-3OZplXZerQsO1WSmS{!`v-tf5_1eBJy-<~G|U32gA8@Tz@Vh@ z*YadYQf0Bll_qG%%uzZfkjr9V^3D?38vF{76dDzx&MS0s5d}c4>qQbzlVSvsi~3A< z1;L^@jlFD2*#CD&gM~5ujd6aBU&E|`?CAcU$D7u->kzZ9w+T8u2D3BO}PSa z+lGL#wO(nu__82o=Mnt1l%=FhA4yHkX|aFcFo}#W1&KutNwu=-rd2wi`z!zRc*f)N zs&E+Thn)>c_Yxw~H?8(M$Wf7$mv7QvK0HAFVAa6(Ff>dfj6sY>P4X)^@u7-ZX;mem za$9L;*&oj}(Hvoh6h$`WC5=n@egQ`T#D^f_X<%*q5mP3@paeH?|@KuJ5(Na=UoflkIBe?d!%oy#T3?TyZ zuE9?eEhByOT0|Ise}Ck=p@f&CJJL%f`G1Tk?{@(>JRa86hw{K)B~Rh0SY|dvYvsntkKE%B?CkcqtD$x zUrx#ZZll8N6maZoag9F0h~Nc>tce^Uy5o_>NYwvwQLfTgqYJ~ycAyl!r?b-|FpVlf zSv_fI_B)j23OEzg>Mt`?yO!`^1VrE|r(lYK_>(-S>h*>w=I`H28j2pzn6bKq7xcx& zd)D!qgN9trFYTBaBxC>^^QtwCeH=UUH5So1xfYP1@p}k5zt*g^$=l5oTePAps!dXt zAxhIRO{jPtFPAEluqU9sX=z2mdHS~hzr>h6SIWf81RDj_>79VfXW5jnld2a<0Bz*Z zKnFZ${KXdj>*C}N)bJ$#&`RKJ(%i9NH|bPC!Dr1|Zgxh= z)vkO6L2p2WDM=B0IlRLCp9w*TQlrhy>U&B;*H-Jt{R!M!bNJ%BKl1niaYs9QSHEXL z(4^0o&qGbJoKjN=xQbdfT#(G!D*Z%%?|*M?M-3xU!77^%GEOnU%7Tg{-eA$KoL5MK z&iLSYrly(2hvbZR^Esy3SsOTg!ainfo619QuYRu?D+<^*ZUMQtsl!*N95t{DiaG=z zmal7Q`eqwsRm~(kKQ)Fvu4V_OvXq061lGL?>J!(_tCjB`eTz%EX;DCb zBh(xmu(RYxqLIw1Y6v0^Cp|H!)zZ4VNI>a8$LU&5$XIp>BSZMW6{q5&f{5ak2Qg#r z;nY{slkXDqfMK+eD5!@lGfa8Kjt^eh->dE>rZp1tIKAv>*vz%X;Lz(!?_Xb;#=h&W zBtywf8WYd3BHT#4@0$P~2~Vfx5==8e+=5ANm$j|_<8?honFFh$y3y=)lv9*t3-hsz z0v}eyey^`MI;?%cbyj8e3%e38Zwgen1YBO-DgXORJ7N*9TB5ZbjW=>o)lz_CMNFNN zjlrK|gILIfR~ghN#m8e<0Yi%SkH7-5C8mwz7uxai{>tII?+uW`XeYtXSEoNhG{0C1 zcubDwQP+jUJdZyYXqch>p3H|uBNLisZ7lBVld+#J8vd{uJoe3HQ;J!;#Ts^rHrAfwH(H-c-!&4`|3Xr4>_G5?= zM@?$GBbQru?)X1Vk3(8H#sy%&HpvvhLpnV+tk*5s`yz-Y9@g#tHu!ETDJ8Ch74rDU z>UB{{;@IIjm>6R|;ToiAG)9U|Pq?)_-OVr*SHlfHy+RlRXcJbC$>`3D@Sf~*6^h6z z=)PNk_j?}8F#0q1O0IB&ZGMJrNkmZ%@mM?XjsSCjeRzq$o|j+yOj&9XVz?Tt#K>^2 zU_GYb{_^DH2o&C8afVOm?*@-7H=Cwex&9HDXSZijmEZ3LFdMgXec%gWv(s^2)X|9KYWMeHsoC|0W}q zP#c+4CJsy(jP~$*xHF<3?;)CVoR+{mtBVNl(}2p=M9~0$FjI>0WVu5c06rmiXQ}?65%ks3m9lWCB6I46G8j`pNKr}rRERLO>p7IF^=~z zR7fCP($zG#@$&m|bd5$h>_67XYw8ns;32?Eku?pp^_OktMi zKM0s~KTR07S*J!XfIWJK_f-q;Z!4_^h;qY)*`P-Yjf_`my^E~x zVZh_J?QK`kyEAje+B|BH(*oJh3M}+qVHE&p9_A4BiCg^MI=f$Jb(b2JiW5CNfgO1R z7}yeb)aiUL&tp+H87fJ)M4if?3)cB~BenbY7q zh`y;YA0B4VfR^Uc<+%s-3o)9(NVi-GZ>i29eX?y3^tIZ3%B4_Nkd})p0ufGiw{=zw z*=z)>S-jc`etV>omEEsy{wdmw+;|VlkMDS~^F>JEaL4ARb!Vazr7z zu$1DIt&SrS$PzuIzC6Dq;$*f&vdf_qPVQUX_e&P*ZL|TwH>;*O(BXHN1n3^?_vqJK zM@J*ir&jMIH^S4f2WCsH$39D*TDRL!m6MT?sRHO;r+Ul`KhUkVAFJyL;TKP7Vw%ef zm>c|m9%^ei%718g%IG0}-r$6g5|ZysD=*Tt2aB;VB^^-*aiv`+L2r6Nu^V~XMHY$W z47bV;x1`FF2)=huJv#`dIf9+&F($V~6s|135nJyd+R!<#1+au8B>u zI%?)(XI$Ba-Nf3CDG`@|ArboH=uZ%U=zX!fz0x#(w*^^(x^wh)sg(-d;iJX+*wGO- z%tJZVZ}Ow&=wq}mM)q^f+fJ2}Og=n%ugwJVfQbGv5!Rg*6uh{{Jh8lIp|Lkq-V>kf&X=QOlVhLXjr%ZWal>k@9sk0&OW8iD+iY zM9%o(bd4Xhwn9rZp@P6K3Xwd3PL>>mOY{AUfRrDMthS}F`CjAxa!-L45Rm8Co(l@< zc{7qK%8@a;Xxdi1z4ENkE%>rC;j*5jpk~MOfwQh7CXRB`_KlLhF+Ro&|R>w!0 zE@rU_bvxs&yS*$DJ-z%=;Fj<`TRt=*lu?PTU=L)3v7w^Q(#iD=C|W`tVk${kjiwW< zHf3{wM@az=LKV0#1=gMea%mP}9h4Et@01Djs>Db0)g&GVlR@WaJ|z5!(0>XD|A0+b zDR_-=5vpkSWp}E6J*Ff1c(IFeb+(@Cw9=B#FA^Qlhky-hX#UxO(i707gF6kt!8_9v zhQb6K1AE`~M9FQ6HBtbamM%5RXT66_w1KdGtg(51b5jb6P-{Q-R<31}4n;#L$Yt?4 zazC7DIbwpH8lOo}jk+ro3cV<1XKXmhMM@!o<5b+MRwv0uCE;N26#|9rX1B-ZFSuoZ z85`NeuTamP17|HM_Yzf{C9z2OgckO20nh0CSc&V`v z<7EI@P_pUQ^_e0fSXh)hM4)Fv#5K@JTHP7&l=2bs z<~vGxFc8{X)?F9+mz%4bFyd}ch7nRS5{7uLxXP@=HB_O?<(6&`B?`Xl?N1yl5$fB4 zRe%S8qu23#YSge>B8dqSfQ5Bg$VFIt;HxqotrLe_=C->?67ar)8_IVIQ(~bVq>^)h+ z@gR)O{UR{|h1~ZCWJFjH;wZ2b2=xg};u>?ryrC|ewA?Mam2wDx$cY|IV^{}zts$R1 z?aDm8U3^R$2gBL(lgA==dJ^4)ieEVD6rG&FE-m-Q+i&~lW)+%kO-_Fdm$@M4y6nbE z966%ekOHMN=HiBFM*WuB3o;n)h}K~yhcm?*(4mb0cmBvX3xjyZ0os!bLPOzv7s5YH zLBX`x1nYCYKhR*-DvFeKgDWvqrfhu++(W9rkk5XWP3Cf%1y9}CmU99Fs8SYR^`gV^ z!Z&Fuf{l;H&eB!DG=a0+Er#kPt2q1Z_hCn#<4)-Kt~)p@S?U zFtkzl`SXk3U2w(?4r<>;;}KE2vl6`FcP;@ZxuV-V-VeVHC@7jdDs?KR$76MV{>hS( zPCX*TDn=O^JO$kJh#4ru9sz#o^qc^#if2wMYLe*^h zpj}9>I<}PCfht$|QV{elA;g8ksM!vO*I`a6{X{zRheD0frJ}|>Zorww;!uSJUK~3V zq0178Hr>1tZCM2}QLDFCZCGsY6Yprcw!Q-5Q^3DJ6fc=a$lj8p!kh~6r}RFnb?}bR z@+SaM0d{iauc7l+A96%#9@At$f%oNHCQ7qYRH(&_dz;6B1c+kNH-9$?^h}R8^k^3o zOz==T$slnMTj8YO6*r&4qK^S^uOkblj!ssYP%?TzM|+})@c)M=zu88CrF@uY)|NOAO5hE*(1`rp-z-f(N6 zbuq#Sn1m5a13uX^1NY(@$k=`f3FxzCMR4i%{rs9_Y+#Nm({-H9a3bA@-_mm@8nW^v!>@F{jcgps7hfB*O#{ zrAS|0v8^mOLVlJRP{55_qY>LGg)`j5lOKW2QnS8DHNMq%AHDJV9Fv}P%lK)aP#&(_ zm%X>q{se@?u|;OmU=bN9rH()*(o@erB{5T+G zGsRpB_Wp8~@k@+j?KmXl4ya`o0lW^}1tiE|2AIwNsC*U=5OZApuJeUPaHItMakRK> z_U!EM%Y$g6?;n5owb^yx5+%*woo~W=`}lyS%ClczDXtD@A41|LcXgOLqVzlW}U;JSd^%~lLS?SpRCN;grEZhDw*;;-45 zLu%=Zn74{DBpjZCRZJanV$4C5DAPp?qaeWRI4L1Ol->|oIQlm8!TlvJ%69rQSHpDV z`}Wr7PBM%|+eez%10YlHELNmyz@TF^0sDpa{^=^(NRB8tEN8a0B2m~Era~Da3ls)T z;xzepoX<we_wnN)9p8RW;VL-K;~qCLeyOz8ZMFX#?_ z$p=++7o^W8#@0zJ2>&r?=e{R!oL|l;<})sz6~ah%@3PGaJM5t?!_{$Z>EJ(APd*H< zX+>f20Ap74rL@#(c|f#6DTDicM7dB)Rx|&&NnM})4vT2u{naP|v7jYl*eitk7WH(o zj->2L5X%ws!}*H~c9yFsY)J6?sF@@+n%*AVVveaD=AlYT&p^{Yt1=^bc;Sd3R>bK% z8vS`lIS8mN2)ATQYAV2Qw8Oc&xWM)(98*sf;svHmy=Gr19?zNqJ0#E8 zv%gX)kRY7}8(OA%ANR*J6#$JT+hpZ1k2x!TS=TaCT5h#H5c-!Ku$Gi%FoK!+3i+`S z2wBYv9r_(4?=C(8D7ZW> zs^`Z}Tu?ViRvU^f4c%!Y?KwIob-AqI{UvOiEiEa^QEQs2}r znk9IXZJen3emDh#q%@L-F@7CQDY?h*>GJJCUinXbO#Z#L59cSKl=Y4wo~-hc1dmI> ztpG35HZ@%Vbv`re_6QGPZgCUbm$%iwYN@Kx(dHVHnCP+ve0I_pb)vUw-agrv+W}xd z*q~m^aBU-i0;rzjxR0^+hPOWz67C`vx&7+^JyX%l_MnD|=3|=DUL!l+9tUVG%Tf6|w0|v2(?hOz+xK zk3?as4Tnd_K1bZne=6=84ASKam!FsT97hU@8Z76SkMh5nYoF{%fcZRA*$U-E)jXce zJ+etskx=S;C%EXn-(KQvtj#2ixLFPy?uRIDp1oz9(ZIe5m8ZTF^OeB(UuJlXzuIzM_jkDVfvE5SYoB|^p(Jy0jz<@ z!%~&RuzZ(?GYr9yABLluye!ZkFhyieKy42)7tDIKJ=%5_60X6f$~$oT3U#;#kq(Wc zBd&a*fsxD+nU0{U>pvL`P&hF1=%gB-?@6zvDs>+b|A83f z>!6+e30Q=x%)U7*i(oa)tI#6`ED~Vv{Z3dF{K`+5epy&S9xB=CTY^~9fsr}@e zFXz8lQW5?7B_$20iyXE_fD+mDYf{|;@@kd-b4z=*bQ&>x3 zpol?AVcJz6xC4+nhOhiSK;1Qg82HelxX7SS*!Xy|yPuPqslP=|uz^xC(UuSDLuiO~ z`9SGck>xOO#|OEafkw*!^KwsX9{wi%(a-jzLZ|23T#a75CxsC0F#@_k6zaKNr@5>U zh`@|4~#I>AZ^FCp%wkccodbla`Own%()&tx5#3ufLzE8cAkd-Mb+1?JOnKmQ@f8 zd>IJlRoWFRrOwE_Qf*9=u=cE6&IR(PbjJHH#(WQRp`5d-bm^*zGZe3C>*2O}c*)c%^yT22h?z z0j5dBEmM!-n~TVc0{QK}0NiUsF3}Z`$mVw z5dp?(iSkhC>y^nIfH}xZlJFhm5GoqBefU*Vg~SLpEIV-0ZgOko84SXQ0N8WVod!?H zkTm2L7A7Ibb8kn{-l(k!z!+p9_}o5B@`wm!)6nQ+e{F`PDEFfX*XX{k{!pi^b0Y?) zEoL(D*7GD;T}1qE6GrgaRWv-dYyUm|{8#iZx1%1ae(C7@ zd5x57ME94iQ{6m){+PBlW!GE~$ctVq5wwMC8$>OO(!ZK-t6cuLF0{`76hYORwzIY( z>>!ST!I_+0|1)camfz+}V|=do+07~M2ABB|=AGLls|TWlk&n zkDLFp01HuBACDKwk>HSn06M4)pdi+aSu|WZPZES|$4cF5W^lSngG0jhdJ+!GNuUDs zDz|wbq9UlHG_!PRAQyBi^1GVT%XCa-H#Q6>a;eDSbx32*H}y2-76Uhl@XQX^?QqDo zM4oY_F58vCHSM`bz2h<;5fFfu&$NO=LW)x)_XQ&fu1{Ahjff^PA%-(J=qrOCVZh-Y zQY#DU3FtOT_-?`B}O;PD38r2Su zTsBNoX%EwuiPI;B`Scl!Q9Cr%1N96?ztc9c!FFM?F3y#VRMaj>ViNP<&+=$`MPDEh z{*_M^C2hYYf1mhTbX1|d@;bSw%5QepB-%R9zt(pG(ywNgE2XQSeu0x{`fKiH!<#0DQYF({-{ZA!(sL^O-;d}GqH*1(^D0B|m0yRUdXVrY z*}3oEpHTG{351$ksOrO`-QXZvxvq>M`u&n4%0c<@XfEeofhI!G?cJ5zFJS~Jwc1(pa{cmIT(+npY zq?UReej=ygq#A%^*|R?vEJ9WNmSCWas?cO z+$_6B-nGbn#=;~X`y#Bj!HLJGA!oKbl1ly|WcUK0LqyNy*h0`L_k`S*kutHwbKC8; z+a10)MMkY|PR&6#;-H5t85FI5?>GaQw?%-WcLSxF%{Tp@M>A`oPQlr%&VG(rCo=?{ z%ZcJu?nwfpCiU)+X*f>I8YQFU`QoP4;ASk;F*^XYoQIgjdFs#Yi*JBwv^?EL-_dDK zN!cY_lPCRu{iX09zy4(zf+@jxiE?tsy3N{MbJJ$ONBH1cxK*>A-FlP;$M0x5R+it z9_`{H=Q9m7l^U}(HOHTL9c0eyoD$J|^CRFaM~LEQj-Z9XFa2^6P*OFjm-&VdiUDo= z@A(P;`VfKfcfzL953_i>9jqqJ6Tf*+f3cd>)C6Eqh;X90rvFpHL_ z9m^K~Fwx*-(f5f7K|LoWFBD1DLA#Rbb=bIjI{TIHvTI~AmqFCNPoZY1nKW;=y{Bmy zFI69eTCR=tPQ}~+hs{rBf)tQ8M{-)Qo> zih>qgw}zfymMgY^Pl^@;3ZQmMRkg!7ogMq#mSx9VH=WVc!$Qq>Q&zGqGD4*|3x&NZUCV3MNCw% z#NRp#WizC{Xgp<56h)W&@jBy|vQp?y0fS{%=pNYg-~+O-z)1anKlDw4Lb0y;1h{sYu1|{V2@uWK zSe4Md$;f0^p@XOt>@j;2yw&J+Yx#q{uil2vSD36T+e8F-*1H3TuTjaT{2lEgCA#J7 z6C9NyYW%SrwiC3@s_v0;rw(6|k z3(mh9)kd?ZD;^txV>x2R_!Al1Q?Rb!_-MJ;6PEUM!D9d|8L>8NaDUQPqAED^TvvUA zX#BOt%^(49)yC^e+)p%Wp92FMTgAV!qX~;4CR3UQjz+JIqSa4)QG5-&sMh0S_7l6F zYNW#SvG%{N75s{SA?470o5ndsea6Lngs_;rtug7aw_p03&OuNVGXnfFJ~_Jl zh|u1RZo8Ier>o&@Qg*>;%S{4(Bbz`zO>di z2$QyQU{ANz%*2nB<&q^$>&`BBbRPN(o zOFMy2TvGKD_|K{~tM?`>2HL^7?N3xIjmX5{k2mo3^2tzzSlKReBDE`asZ@5OW^fND z@jB3^B3+dn`(=aVTV{FxM@>_~Hm^02)(3s zP+43wU;k17A%&U-4Zd3k*la%U7w>v8V1zAA5c-(GdKepr%W7C$iX4l`i`6=u*uWlK>EgO~q?2y4(KgdG-^5>IoLtu*o#7uJEKQqD{WKf|Wi_J_JUw=y?xov(^N377 zi^A{ou2mc<6amKNM;Py;E)Kp@D8k!LQVZ}VDF*7X76{VJO-Vr{YAc+^E|iPqw&JGv z_(i}~DvD3zT7o5w!^ElVD@gHU%E?L5M*M=&2Mc`Z$zavgK3!~fUZ3|6fO-HPwILOR zbu%gvi72ELa#wtS$)p2X>Zmhb%76ipMz=~opXHX2@7?DVzHg88N-B9pLqICmH}6@5 zZsQ+3jyhUo_dWt#-+AeEN)a72G8cQPuT3rw!7p z29xS1UDmg(<0L$s#fmES7R6${&2Yr?RWEk!-Beo6#b2%R7)&Oub;Tw;RfJ%G|7p43 zxswi&<33d8pjlV;LTm)VYM@i>@QP3^m2kc?Q|hhCPGEHp{N0Q?~d7?qVlx-)8%<+~4}RMP*CvA2MVGTQq_iJ>H<8|jknZjlaY zq@}y0b4ckfK|nx}6anc5>6VflN<>0HQtIwG=e+Md-@5lZp0#EzM}>K2p4t1q|M3ez zMVlSo=V#*Rh)8}5L=aA-OGk^I(IT@Hmh>!sc9igN#(s<5Z33hS)mT4<0xhN{+6=G0 z0P~0aL4Qv1(&r}Qfc$|D4R_Z?aJ%o#%=e~W86S0&P*KpX zBjvf46<~_oK_^zdUUe%X)2roZhwRL+nrOcBiWLpKdaD;*s#37J|6a}hAwe1o+gz)W zkT#{AEOQ2uK<(Y3o1%X;zjZ4H2Yq-sA`nFHsl?O@_lW@Jh$g@Sfzu19tvwN$mT z3Q(ziQr8aPRb<3!(pU)ksKVX;OU`$kjAQ*u7{O z=u3lvIU9@ITM~2qIvX_;9^^h1Hm-Ln+9|eWv56_xaC(B7&kDa>_{!uc5Y1fOM`FLG+#r2A@+;+MmATD%-z5g`0DD^p3!*W?w~PjR=wi^%Yb>+eq76Z2YH?0ExCzdHou>TVY_?r zXtuba2(K@BwpITV)%a{431>R<^Ij{vIevT9PqS5qAM;0_H%$sUJr{6cS0Q0P@|X0D ziQB6-af2dH z)?j3=W!bB5HdOM-a{H(|+*EBsz2{l_ysRM#8>!V;irie_?*#py`K_J^vi? z4Gh~IHlL~}1WOb!#X^*JHo^QBB@azo&qnGlwHVT;MKyZ-|sfl zM-P9A;L9huECv)zV$q0;zxHlAcw3?Ln`Ek0hjm`s3d>j-e`r zLRJ}SKnuYd+vW2nRdc{ogtaA=bQW6BCgWFQfEhB~uc&~04HSMkqF&`NSH2f(9YOHi z&LpM#y;?4VX`63UOOMDJ&~_5Bg9RZcssQ~C>}@=MTk(0KIYT!cK>lX5k`8?+*$A4k zU-!k7(^-a_-vUW|*V{V)wJ3K&Tc$oO0yL@N7{sD5@aL{*!|AUzGYqFutiXriuPj#E z{KAaKaP@L?sF(h5AvRgd3>JAM<_x`(Mdh=v<-nh$o&0PfmLuR*&KgK3pU5tiFKRt9 zo94TY@~K}cm+id{!zS)pSr=y4Go3wi%R>3S(VGKA;lN0v=~c=MU@Oj6N1Ykl*ej z~i2 zoR=7$pZ+i%TiYM632T!E?IS%&+EU-+&5_}=J&tylxBCx=@3YB12*mBoKQ0Jyv8j7s zANk8^Ma)~LN3_nA^u2nnUdOlQuO~_d=>=B|8jqltBeA5C-j$DqtfDWs_YHUF>q?zg zYr0cko{&6dfakgp8JW!bkF!6{D)bpGV+o_K!#BrIy!P5SXTG#1#4@T31F?GSB$*1t zR~PR0JwaVK(BlnJo)>vhXP#(+Q&16|gWONdG}3Ni(q7k^YY z;h-Y~i@yRs39NW>Kvv56naw~7eQ$|U8iP_Q!~4nA8jJA9w}m^IZkI*+%7Zg?$vAlrwic>RMZ^t$#~?V&1t0Ljd#{a*Fn zB}^oTtK(AETf~4y7bKEo{C3IGSkMz3H}6R{s>p-kEUsS|lF`@VA$P`LgvH3FS2@YS z^}@f@X_5m86d`&W@8U&^v=uI>d_;?qyQ!WpXF|joG+AubMcntZqXRnIDcIfWtR`F* z;~0x$*1t!;p7s3}Pr>3+qKWh39nlj^Z|pXNY*E`ptI@n~)vSw>AF0-Y*>nS)++(t0 ze9H!Ltv^T3R@gKV_r(*b1Pcc*>z#!@^C4zCXd}y>F>17qGyhU+^5QU_Yr4Up0B~Cu{Ags5}(FGQGgo8`T6vt9ymW)~OE}zQIK+>b3Q>C=@goH&`K93iTX(-?>G1e_u&^tzNV3msYDfx>yi*2WqV8}`M@s; zx2d62=rG`8pQQ%`;-%Hn*FGy^*+m~;JK|)&a#^e|OJ~zp=eDBErhfg#&K3@|J7k2|*YZz&co4<4ziv2q>SZ%7E_aLHEC*Y`2c$G_4o1 zqxP|WdNCl_>X)?`9*S7)$Fd$?tK0N6mrPhvSbg=uvWkOFB}=30N^PZzuszlzN#hKw zX`^or-aEpaO=dVhW>r?WsyqZo-*6S+=Xc5X=0YSNuC?^SOX;y{RVY0&Wvm=hSkm@L)*h#S zBsbzVHLw_A0KT;Z>G5uautY5;TKDU7XGW_~?Fc;x8k!1g+;a8t4HTci;Oryw95<09 zg=d^J0D=Rj6V{fsm>+{MVs#f)qz?L{v)2OCZNWh%dsJ)I#K8eBll5isIpT)qyTja1 zVH;P!Q@K=#dmFm)ZFn4dAfF${9-AT6*hjey$A4HWiBZjwSc-qI>S#GT{mvn5-soAO zn+1?#N_bZxEDfj^j15OQuOyXdy#)I|%Wen^=-Y(1PUnPWvL zW|~>N59zZcvjlQwM7j*wiHT-yh&( zJR9*Gr(DW<-P4Dz7iBbt!`SE#JPB%u8y!+{)J;p(^O8T#S7&6XWEu$4hIwM8eYuUo zN;x-a{j-%GE{r_yqA_9@$&fHH!zJXZOqphu?nm(;0^DiRv?__RPU^R9!*+oTdJNJ$G2WM^TCjhG3tRt&bo5qp%XIgW8=C^b^{uK1#dWA zA=b;<@EgraBsi>DZ(x8WL4YtNo4)v*k&$moU!wo>Pl^n{-5l1g| zo+G901Ck+M{G*EdfU}?P*s95d-3oQMyVp5h7J(I*W$Du7kjwENeFzT4ZNWif;X~?w zyP^yGFx}*C357{>(Ww5IDN6$*eFgPA;kT_T%%FJ&Kp&ZkDRj53*boSOYzB0s91f!o z)=6LJiu<1`GHK+q<0cr~a`u(;Kpa|eznmVf7@qloIm@^&7iuo}zrVkpn+NT{b34C&BRyh65q8LOMkvM9EEP;$#- zG1sV&Bu$>`Jdny^tNyLtvlQRvQm)={)mVhYm!9Sa@G#8K`Dn6Ke@N5!{IgR|mn`M^ znV$MEtvxzVzk1Gw_H}Sw#FznX(CNP6*Tl$=i7uf$X)K%*${sVw-*-6P@AKmoem_dTPn&Z-c3< zSDeQ%EMq4RX8$_&F*WXh39kvlv0&{GiU#lj77F#CrSmA=ghevY|#>wNN zf#ibQB`*lLO&;H8ScT3v-s|CZ#reJ>_1GSBl>fcnpf8kM6!-S58GK{8C@1$CBi z-I2@DZq!9P={`*`Mk*e23yDKFCt@1EaP7S6FM71Vj^35od_Vct)CkcT)vM_j;aHkN z#_o|`S0u6vf-ur~!j?Qey@R_YYEpP?|Lwv*7< zB%%5BMpo?G7UZnd8y$E#&#T*L=*G$3K1X zDD$%@lc76K0KMd&AM}iqT{}1gbu3pIOeUfNdTle_#J-$;o_{h%vNDKnK3C|^Dqm1P ze3ahW=-PVfF=t4f7@RaaQ%B6nKOONl$o--GZt7HbchsW0tt~o~&vt~#fUy9uyN5FA zqf3<2o@&nZKP$BAn`P7A$^v|8$Ej-PB^@$?^yf9te-NdT=$#fr6vGN>e4?CwHn{P0 zeV(@K(seoQi;=v~Z*u-^11!~Qw+lW`wiP{%dtXh`Bxp07XIT4vkakF`M6>!yBGL}o zlW7GUDlN|E(EtTR$BZ5}dO5y6%=6_pgwoqUy@%-eaug&U&lec-b$b2kZFu=}hSh%S z-52`)r+AkEa`A7({XBLTJ{6m9kRzFjMbayvs@75rQMj4>4;Fxk6lREXz;4=R;JEeh~@{^LKq=sP}|^ePg_g@J<-8bIY2&?ryF!=)>nk5KB>- zIANZLE-+Ipg;*xF*KMeai5j-!tQK+i(_+o3qFVO>oUfg@q+^)gr`A$eeJ73% z^*^SzOU;5mz4P+fV^Xsky19D#tlV<1O1Cm94SN6^w(b6=T-SQLtueS zc!}v|Kd0J*=V_5URf*(+s!^wVJATNh=kZj`iTFIQ<_E-=*#_9R_$XW}Gh=hWB_8CA*_FEtKg$qg|6e} z^6l}@jB?AkX$UT{AKOv8W6$ZXWM@MdqH!e>YQI8 zN8J?f{M}Zbf&(uSsE4YjPy;!-hiuU z!<3%P7lFX;SR$<{rFNH!Kenw5xU1+eiTwO7jEO@5hZ)?;4x7tNgv4jSY%rZ3F`sdt z*k2YEo%CruDI9FVlg_KRQk)LW|4;{@Qd-R()^b612Uy*=z=+y95fkSXAR%NBb38XV zF@f|zl|?auY|4051lDt$FSwyJ7i5nD=ap0FBJmULGB%zL#R4l8jNu@=FF?xgrHVzi zS2DNYtE#An)SPa6=9l@>8+|d>0F2NQhd>2Z;#fYYM`GLq@ZG|@R##fz1s-p>?02h; z(S>sT@U`F%UgT%Befv4Z@am7J7+3~jwJ>${gyOUUm!_m76p^5E_ zm1H~UJW%Ue-sg6QTS{FOg@izj(mGL1d5Z#b8kJQqDVW8t6G(;BtDc;BwFMr_YV{?# z`o2JS(OILCWRJmTK0iywx-ct}M%0eTVnI9u#cR}W_s@vS(JDlo>}VziZ?_$Cp$$}h z7H%@o;s`3FZAZzWT>Y2|Qdj)Yx)mTayA9ejft;evp4SaC)mri*NTdmGq;AY9NS>y< z+iMm_QT|y+EAD-;a7$YV{faA(g&6QkggX~sIPkLOhuV{iA5(JPhs*X~M|(A%gN#gv z5hs?A^&|>+niEn<$ZOv7CyuLQQsN;vUmpX9GVMeny*>cYhRDVM@t~iAN%&Z9w1bW* zzw1h6Q5gb(InM4}RjzTH@07JTF41_tgi)g^Y6RBV-n>4YebrG^GUBZy$g+Tc3LC&J zoxC0?9>MKhj{-_;S)j+^khnY*)K8#8$m=VcaGe~A332;M#bfmx7P11f7@Zx7&C`tAseg|&pQ`@0emBK-b3~--rPdc3%9f z;ud0=jDX>r1uw|9H*DPH?&hF?s5tE)7)pgmOv$nnn+T@{2-860)(F`9JX z+{LPsIm~q9A8VbR);)&l+@Y_wKz7_BQlgxp{9ZMO4wv5mFS~|Iw>;hE>($A&wzbgd za6*?Z()a^_?eN;YBEi98DP8}-A5k}r4^N~$wucR&8wr9uf=#^UH_RIOgrl82feM8{ zmw#Hb)aq05bv!ef_8<&H;7|^U6Sr%Gqq4AXL?y}xy-N#&YqBO)6r_f$yXozNU@Xn3 zABDv!K}pt4^de|6047a82HEfME$i_A#@oBu8#gvfA+*>NE4FWL?zh;e@GwWn^ZmNJ zg8;%_hlon%psjAX!F0#eQ=h==DjlBnNWSzrsHWe6+`&7jPYk2{VSrIUNr1fF0_Y&%My^5 zn!Sq)gSr<$Tzqk+Mo#``z5Vu44xg#dkDifQ#CHFyQO?!sflj$b7hWA(fkY~g!>{eF zs^onAIAxdO(d1rx!#`pOoEaCX!~&bJ_E(m;bCqYy%WVBPDy(}GDD2EZVLDxJDuK)a zS*<2|te;8tdj~C|hW{uLM}hDWI($-s&!AW+GE)E)WdNvVd$v|*9ok%B*%wd-urF+Q z$dz{oW08Xhx1C)5@i5M`j$)>YHI0qn;Ld?ahTv z43zY9v$5^EC00FCPYalmrgT(GX*rQ2(=U z)9-aZL&@gwLsL6}+d+8;Gm{0`Dr0DLMZ{E*A*U6KPW^}SH$#>;=f__lA@8Q&!3QT% z4}$@acotK-H5RJr=slWcM*gT&4;c=hi{Z0baABnNi2UiR*clu~(*6{=TEIU~pZ7DnN!M+UF8)x#=D6Q%-gfG?YlnD7B9g~{qff#8k*7`gpaArux&Xibl#)81lp zV5uVJGwu6M>zx%)Sd7vRHUE^OgyR*JhVd04Pjq*Vb(qI<@D-i|V(bcp*@oH^%=W#R zLVscVTNfTZqF{3ZEN0goT8Nd4@Arvh+VFbEd3BHPqq|<+)nb}dBD|{E+%mxH;=8)1 z$KkRFqh{A*mv)2#*~kY&ukh&EJEP5O+hoE~J(P;;If&7Mt`8cKcCacoK<%tNSw!S! zg2}4cUH>7Ew_8tKzFfzFh^>G0ND7I| ztSuSH`3$CB{Vo&tal52>&T>YLMqoUV%AjI1(o$rTA5`~VUAMxJ)(KN1g*llVKCVNVe5Gbq@g7XLd`@e%U${;L*pyUth`4l>0Tt3# z>=Q{p`p#?iGHuM^aZ(Ibmm9a6Cg%}C^Sn!(a9-wh9`g_rkWY{axEGbE6iABG!LpUQ zydm5?WsMHOlZ7(17QLuT+mt^r-o`(fE!UlewA95|o7MfRWsHH%J+!NAo*D%BG`wq( z7ZkqlyS-WVUbM4dyS>bwnplD-Trf9aOF2TK|6Bky#O0!HLNW~z z8PyJ_d+PSfR~``dSw#*Nkkrv(m_OYJoUAbZs3B=m^v6)>*F30rk?}ZHKmmvDga@&b z@V?$$M#SSsvUN_Ov7!*c{!$pP$K>w#E{!pbMCTr&c{3CWoDS{fX8b`M*w%= zS}t}sPuEvY6HT0sK*uU|_6t|o`0JZ`RWf)xrEcspAeSIiZU8vWZO>Nd3r@_m?yid< z&et@&5mj2(kow8 z(3)`lHUDC?(0K@e?nC4g92P!t`p1)c{Fq^de)yK_(!l{@$GA;}AA;l=e}gZHv1OR;w$q8?8k2mUErpTHacG6IaW*qaZ? zS=7R9h0;r*HMDJzW$D)dPjJxc{rT(|gKHpslL3m0^ze~5Kz(i4A;ITP@l;}&HEoIr zVQpY)U!i1M*%Xa1I=-$7ENrZmGbH;XKfyYpF9t3tF64xR_m`WsiMdQG?|!jQ!b#;N z&x)Up3kn=uxLHe!M7F*qPOuQiH7#kl6k{27`jO$_rZ9*=3RIrb!8oAf3nM5Xe|jFq z+M&v#ri=NVY{f8{rLRwVCUYNzM%bgTanL$-9~ZKP>!zPA9ZbKAo0)Ro_Iw#~%&!fC z0OP=|VizJ5bl{ah6*q&4A0+~*spYjjT-Iv#_mVkKNl!Ch2uFA%>!?@GU?k{L&+xWS zDQy_Q+>z(-m$B?>gjSDc^`mloNWwjZToeP}JdcSI<9-XJK0ipCW}5E_=QueAT_Y{N zTy99xYiU;Kgyii^aZv9e#AqEH{n^vR6vrG;IjNgM)2Ltfhhl3?FTz)kWoZMdji=u_ z>X$t)j>6_WQF~Adkhf6+j>B9y>>@#FIU?;nO79JZQm0*y%sX-G65&(!^IxvMM^&OP zlT$Va0OEQcdmm^WKTc;^pB_- zgDJAg2<#|0Ubf$L4)a5e__1b#LoX`p6?K4HlpvybleKETv3Zsw|52FzQb!Ozqk~Sd znqh;~&z+*g>YPvJUmld5TwY`>*B?nP571gB^TS_A zQOTR(zmN9>97|M|8Zt8nexF|S zLi^ZPF}Ce#K)Nz59S<;Pioc-98DhlnUT8j~=7Fgz9bu;|BUwDOIX28CP+fGws1 zmFg_!5AA6=-dvz;uk!>Q%1n6f5F@VXy|9G$9D?B94?q(co>8;VV9Y(?Ya$j+X(si& zA9`S46lA)rv<%(eXGEfEr^mYK9{F$fV{fY?%Eaoy0TuHyJylOumPv8m;pPIiz&wY0 zeC~?dZ*B;$?Ks?Q&E5B*yRO~lV`IP%Vf=ES*oBSc2uAu~oZ9*cO$Y_CEmS(_PoCe& zhnBgVIvXN`f!f5@+lizc?e~foI8>_oQiAs1wE95{F$6(!^k_eaPYYfu|14^BS~CTF zuf3D-O0Fp9Of?_HLzD+3nLr-p@&ya$#TDr42$0$I$Bi#voecG4u%U1g#&^*n@DMOn zyGwLFMrg)W$Lg%#>=!zJdw)`kgvF^wSOF8-;O|aT>`9` z!eVqnM8}yy`u$V)BPS>jc_XOPQgXcyQY*jJQ8*0Di&^TT4m(L7IrB4(XTKdLUtk4b z43@Nkgfx&k4t-3bUSbuHYYx;Zil}#*SFb4-)dG^-i?y5MxCl7=Ulry6N^-tJeO+rB zXvmdm6-UADesDnpbbCM>4Ae$ZT2TR;0P_HO$A81h{jAC5%whZc6(FtLrMK8r`&uM8 zqAp7h2cPFub!z{SaKZ_NurnR)6ofn$`vZm?&zA{u2QH`#uIH&>w-Ma4ZklG|^cH)uSH+Tq3XYBEN3=6Aiod@#F6e;}|+s z&vpGzf2_}5@DcI~OK~oTL6(^itR?h5@SS25Hn`_Cx$8PF)l-L5KEX7=&IjalRL?th)T;cs4yWj<|FR+IBf~ zue$}%U9h~(SR=@nK{N45;h-S77r9{i?(gqp&QaG(=?F|<{?tUnmdTz{l`AwdQL3Kf zvASU0l>I?5gDJto^uyOe=pP~W1JXYbm(?a(V9%8T@Q6bo;lDv5G96@4P6Z#og1JDq znthko>S*NkqQOCeEqyT}+8dELl8POh5S&*hT4*R_oNOs@^jWR{XAYG(*gGl6`}S6E zqV%4g2ecva05Zc?TK?K=PZ<=GC~HedB6pXY4@JB$YvL&dRnHGr4}>n%gg?Ly)8W|w zZuv&Q=T4M}tX+I&!5iZwtq6tSJ41NKJVdVWw*-h`K(12s-dR?Rv2S#F4Fc=qbnO4T6){$ zgZh|Mx(;;Xj{sIv(uW1O7{BEotXEIq07cE?jLht(zdBwJ;zwdV#qbEhpQO>O)ua`< zyuDa|jjcB-fAfQ07iXhbF=b}$G7a?oKYQ?+{5eg1?By_&D$%O{2%+b!`=|%TBN8@b zUH21t(-<1$>?N8jEa4BtN74XQZ2P5{Ek)?n7cn2F`wa}Xod=*LpF}O;A;?hL>ALc% zIG$8!eAl~U)f^6BDf4pFo+cIYD)(O>Lk~pq#p|%%d}`QX6|)aA1V3PZz8-^QrrFB} zfg}RQ<MgMikkjCF3>C)Ul-Lh!K&Bd93~xY_$_}t z&9xlDFwV4F1CJxIuyD?82_Ij8^n<3L#UOlk%A7|DHTr>=SVMd8?Q=aYqxX`Ym+E#= zxKlp~5GJ6<_zcMamd}d#th&vw>TufoVDlY?9ss~f7@Z$3GUC2p;BdPSSHYx=DW9)o zeK(xp=_ruD(vQqsMvP5Cu8x^V3&6<^O;l!r2r^^&RAN@kgDDbSWq-J#rts!voD~0&@Bsh?Yz}HGKsgKLKGg9#!Umxg?pk?b~&=BA2)1l*P#e{ ztAVM^N#?UPt;5108^|hj%iNp&Nn98NjDPgwaoN0`hSC}1YQKcFBIzA7sHZ-udC`|8 zsCm~nN9`GD;x;!^HtV24zawEvvjIh}=x0_fsP5y!8i3H`^^9-^GD*By?zJUgGrf2u z=@hVBV^U>Mp8gtFX7$jZED{T6$Dkt&#TrPnA+edZqEQOHPo)0XAIsBPao4}|U`HWV zr_Abht}0B|G5$y%VeT9NFo>IWC?x_Hxc1|;0pGJgw~TJqVMep!W?AA_?>wG&P`v?B z(g{7lo~JwNS2uMO%NuPp$<`oTL{2!)6IV7oO#fg|TLM^uy_-99mF&+d*5@jp6`+J9 z{fl@w(>|IOAk?1})8U^$F)}0Kd4oDIv^ijXd~?O?^XH|JC_gN~_>04AiA!%(DnKlo zI^O%(IIq^o5Rc~2FEu)Sk{`l&MPmMKtx$;osuGv-%gFoc2=+(NsSAOOin4NCOvAK) z^GeV=Png&D(yeTz5NZ--Bb)?yckqF0I?aP|W#7z3IoM*tFFfnvtSpe=EUwOuw?LQ}aYrGBtBrbh8gRMg8df}7h^{i)hj%*a*q_W^C09f5S_sxEM zt6pA1+|#ZvU1>W+8tSs>r~tM3w%HMkrmTA+FihXrLUWNJO#X?UEbY(U%A#mQISfns zR^QxR|FQ<4B#-lb!zFRC9A4pEfmC)wUNRwwPG(d2&j&;%5`=#%O`lG4eP2ssk z1bXm=EMW!3{fXPV-T#NG7DKI&ju$gVA3*_&sVxN`o;*KNqv8o>$@ZhEPUKu4f?|yjH;AjVV;N+ z9;@h0o^Qp`9ddStvNSNvkOkaAg3qphJ;HuCvFOWm@^X5uSe;`PgDbnM_p2v14#(Z_ zCeTFl_}PcS=<@AkOvnSUq6^R|j{L&2>kMM>Pliehl)=nW(nn1A*ScWz9{sRDP^ZsF zWATvH4UM6Y+<|6O0C-_FuK)0df-X(bGaL$srQbY^rQq0j{n>hiI0w*`0aC6jL87@#4B~bd zbhVx3lEv&^`moBG)rwm=`8o{&1N6iH^$0D%>xuv}+rX=bsB*e9_^cdeEs}nJd41FD z=>df0J|`GL$%nI_sDK7=iE4&&DuY`2uv+&R##uU#(qT&0J{JlX9ZlqaHLST6-5D`kr7VW1Vi8pm1nyE;6M9<3_YD^_tD03d_h^H zaOds&;3#=a-^0E-!sXm`Sv-#C{AYa2Au#q4TdD&BEJltle1cQoIfz)3ERF1418io- z34h=OFj^i{x7Edv3zQFIh-|p#O?Y1hL#N5om^Cti2+Z#m>+T__Am&KJ5j_thvqtYHf2w)}t zJkVJt9?LI(8l4a-M=jFEYM2q5*j73N^kYcf+Gt;Pjj*cb1FvAs-|_^BKBqsX7+EZ2 z&$N&$VtDXKM3e~yQieC3CoIP*_vH|Jc!arDI(;;5p&tZQg+_cIBQo>v60;MPch+9a+SJdi+Z1esne0L$c(~M`b`g`xhO{FbATNL%h z=xQB@hw0cuREaI$iG2y2qpXm2J{vz+QYJ)!Vn=*va)BAQp;BD>tWHr1=I_8K!9OiG zm$m7le2?$gc;g?L>;W+B|6l>$q(L=X7&Mba2IP?dY{au8KVZ`b3^NVv6NZ$d{2)j1 z(Yj-RbAi85&!-p)8Kn)iJ$fVN^LQW@q;0uhS&Xi-5b|l!9zLqvBi{HC{DP4FU-=mg zBH;A?r^gXJ67G|q7}=m*T&cGPMtXxuPp$5CXm6^G8mni@byZ?UNExJ&zT<|P{|1Kw zZ}5$;$dB+;Mac3gDO?Z~7lT;=i_D=068Me({0M%6KoH_TcW1D& z{Mk_-km?Gk69!kyI<5~}Q`z)60TvPGWKv=7%hKQ7CY zeaB_d`H&n>#`jbN%^|Y*I%W!s<^LN%{x_2R`xAInv7&ZZcKiZdd#w6i;ROrQUBAzT zbJzPdKfREb8Dd4nNDgg=aOVW?&2i-L*(Sop`T&!`!Ub);=`--A9R>wiwNlqqP#=~R zA#b)!iu3;)wEq24|Na~vW-pK4_9apo@mbE~xB}A7a(lo$xZ4vt^zyL`phc%A8wD`B zOMFT;@<@o%FkpU~2_&_KfHmG0;Od0k^Tp$J2{?j77@v@^qg z6#w~t!~Yki013Xo^@%=2Kt{y^?T)Mde_Z^TG7=->$<~nN1SE$D1A>bv%u{LbN%ppz zL9CY)3OYa|g)-qUDcrrOdFI5xvbu5)`A-Y#pUdq?1<7_iF9nN?*X}zG9x_u+Q~9^w zauk4_TV~eT0mPr>ik>F?^pjgo12v5`5)gpO0ylaXG_AKMr0!q(Nx57yLBS5tr?Csq zmSLk@ks5uWq<~tdSWER^T$~;WR4r-XRI|6afpsI>&sV=OZgT4eAGbh<2gBflChabO z69P!~jARTC0Y17LQpoLD7Cw*a8&O^O+~oe|w?=5v!t3>S>TjOIfrRcGeN+2g!0om& z9xSssh9%nnxT*ee52WZ2jNDZJ$R|=7^6deh)*ujUys>+7u0({C0y;5DQ$_OI?H8+c z8ZSm8AU%jsfL|O2@N6s4=?@FxTV|eY^Ya8UTJOFmvaCb*o`Z$D;<}I^!D8ixOf1Cs z*Xu*2>5fi!wPT1mUVl%9bHON4z{(|7lxqlN2P>;1o!w9i4j~3G>nxvDzkw6UZ-^Ml zUH1f`n8?*;K<^_Og!cd6 zJ#dK!1&S0(5$JgQd{Lo@vZ$))yvEG%vG$iLU`%nc9!=fSLEp%Lh|itS1+Ho}o+nak zGe&;(dqPUhQAnI0f`}Y}h0AXTHhEM|Ah`Q!OiOfs|MUp7M+4L$ABHNyzCs;*RY{Gy zObXUAGZpCjf^=mvq1%6J9}3~ekZwC#@mU*+z#!QAK9*aug$e~#5(`h}$4CQ0-X{0S zCf2`y$Osao8r!wbkj~SCCH;JaZdFCF*^%&;1P7d*MbyiGm4F0Z}oU!7S1n=%MsA}AZEQG(Em)c_3u zrDO%kLt|tyRwS0VHsG9rBkn&xUSc=_@j`f)s`K`og4G^3^wu9RZA$>pQLENUi5D!8 z8Zv0W#UX$lt5~hHc%6W@R<5;e!AqLKpYtCJ(jV&%6DtAQbzE;3!+*XM{C~TA;P;pz zQx6Qv<$=fweKwX=n;I5k9ym0{&~I)p);w-5-Qh(|o-&Q^WQP7LP-MfH000NFFwe*9O|NZv< zZ%;z0@tcCK8z3ks_BL?y2p~*S0Z-WXS8C$#M{G40v(` zW!VGh1aL;(6(B5I^~aOZgOE4VxAEi7oCrt?Rnm=k|NV>YNQv5saU76{XPbEFqci1(5@JZzMhcZW47XUU=(QQv08OlJBkUuYwSA2BP)dQgJ~yB7NVVN84;i1N#MjuPOR% zyj=3*1sJ$0JTXNcU5d5I)5KF1d`+Mb8V0KXvW*X#kV!}0q-*4^DRlDbO>$g$D#0aqg;ywqrhhPX$iWdo&Oz`#C#=OR3%ny_ILO@Vo{#lmIBmR^HYJ zPOEJI^RC8`qG{|sa~w>jg=;yC9Yd*=Jputa*_2u;63bL!9Ou7SY#k8O&fwd*C)SD$ zfP1R#H3J5-c58^ftbDIXx!)!zNj6cKhY=FAT?z$9VRR)cxuR2aQP1H~^ znH=*8Kez$oGY7F7u_u~gn^4`&w;@r^N1mri2e@{a*Ys$oaUC^Of4V1<%?3+KDz1u5 z>QUBZ6s5W^a7<{*`noQoai zZ7A0{_89&F%yy8&V{6bsY%8=NQj$8EZDIX_l%zk}nQ}WWcfjvxcyfWqf!KM)@CXJ)pqp$D@F@e%fo;FX#TqDyBC#68*;Qlz{(6Bbu%R?& zdjwb}%l!43$NF*zGR#*V~DY zZiN(ba~vIbxgOmjXWS9k{2Cn&Y6Oo<5=8VVFkpI@TVJ==DfCo@He86M?&05G5|n(P zC6EE!dJXu1D7Pz{eiS(IuGR;$dEu`cp@FnJ{Z5;%zOax+4I#1mdX?cRcxn4OuK(ht z>5+g91Q-@t?%A}x`0^kJNVA)tae~;l+kuOk?uO>KkR z^Gt;D0a;)$diMYAO#ZijU&!hL#{nm3(axxWW8@7l(46=Cb6OUJ);$V;hctNvuRz8R z&qHHLxT;lpzMZY+qO)2ZF*%>h|NU$VR)m3;5k8PX7#js8k^b&VYa^xT%fW{V{ps+C zN)w1Xn8ut0XZ!(vAwSyw57A$kU|C;=iC%*g{;xZa4nE2p23g?8oMY;c8(J`+NChr1 z!$A1{#!g2y1O6rSph3~`=oEoWSP77Df)@x5IEzzWt0d__Qx3)JTvgLcutg$m$%5_C7mT~UF!(;DStCgs1sUAzSgTo_=R7&i05TtTM+e7m6%=Q2JhwiUelUw*E5!qN^2x)&Y_NYP*}a$pw)b zSYYz`x9}zSpC6z(atSIgcww3bszQZ->^>E6Om3AH1fSHU@R-2g7rHo@&swJeQ&L4> z3HBX!ceO)(wfC_Texf(7z*wznR%k6`{~p@S&5*$4Aq5N69rV zjAQz<1(~(^(!$%U00mI~XYXH2@E5Kx$g@g7h@A#Rxfn`NI+rF^udNPE`*DHcylo%H zUvCrmfgapTur0UVfL`Uc2T(_VhbJHc!&UgrdNli)|HUB($WaeF#qaK}T?K58Ff0hq zZCR$=LDv6eXRgW!PBQP_+JMI;^F4L*4Ujyfq|(TQHJ(TtS$JaJ)?Huxs<`6)YV|{bo|SG;8->kWwBQTysx)O_JiJuiF}IzM=&Tp z=^KfFIYPa`Jy5FQ2BLzJi566*T7FC_nX2%Kd$ae!Yq(AW-0)@mBiJz*;{kb_mrKu{ z#FIV)+=jhV&xlXafu^#2(|>X**q?d8>GwZ9*OTzLOvT}{s$0VuDndcF^R8m?xw5EY zlqlc-*7*dxB3KcDAo$aZFVmwpV3@+|y>9?qT8pU@N6*s1dglT`(IW`$4F?Zd(va!V zVx4W$QrZVVvd~H!dWtNPC+elF_tBICBvy^alc7YLWm@l2vw19)!HGBvWJob-)20*q ze;=*OG1`Q7Spbft$JtL^INI+S#O2pov!`tt=~EII*Qgj=hpkT+aX z>haadR}HD=PxHCCPh;@q@fnpFz`QS`)_O!4q((EHCK!2}xNpIUpy<87@OIp1tAq&m zGo`riWQL8}8tRTNP&B*Mi_ir#Kk#ni@b5v_X0KtF8=D`L9f-cm|A_B@S&`BOUEoAW zXX^z$0%T%>JY)cPbXvRwDv+Q8aCq@(BLZXw%`@lVZ;Sxn!s)y>{Pc$p*mVG>MESXB zb-zpb4irGjfc3x#$}4;^Z>R39&!xvecNAk>=#?~Busgg$8rPWZ)JzF`GiAl!2r0?P z1)YA>P`ExI0}5?S14-0A0Sm&W057Q%lFFo3zASaBJkm5RoJS7#hyr3}GVjL0_EjZ^W53I&3I%iA@*HX8Z?DXkgYT)o@1_5l53g9fJ@dl4~*4}h%&6aWau)ViubF9zrLQD!pY0DcdmPZA>#8&K>y z4lC@g?7B**rF{NQNAvXypgd;dwXG+H%L~iyUhgeH8bIB_rzATaz7uXSU2+C2qJ@K3 z+|>WY$wZHo?dBucECRBV&&rax^kyA_ynZKEBT=YAapwn1V)e~v_rS5{L&k|7WEn^F zK_=S5sD$Nn)QRe5$_WrkHsg8retz+A(>Xv~63~vWK#0Ie>@fnw zHE|k6VUMj>BiXc~U|+|Uxlq%-kz+xi?KK10i=HHa$7YzcdN=2l=1|BEhFX4iRp$l$ zz)T?1C;DW4_-s)%^XVd(S)fZCs{B0(V4wjgiBxdR{!A4H+4E~@`Jo8uIM5Or0wGBt z8bty5ru(Oz!8ZL-NTu|{tDM#0-DFgFD>+9Zpy5w3$v7n$_cO}cpHzN@6O(>TTGQ*` zGz!D`f7<)*aIE|Hee1R>TPi|CL|LJ1iAoV}WJb!ok)5pDmPA8CvKsct-aDi0%}qvR z@0t0#-p})WzJL6V&vW$BhF*v&D={5|$9@Olg9nYDV%JFw3#0A{{Sib^A^vs*z zFHR`}^N+q70z~~38^3w4<@Lka*ejsL5`>rh87&Y{1nb=jqtH}tp*Hqhk3{^TM&%wX z>J4yw8WP4jPIMwF7IJ1ieG)C_Wp!NK8lhX+!Tt`Ntd|Ej0YZW}@V3!243>N9Uvyna z8|a{VsiE>#ctXFT+;uU=X1LrCku1arpGYlOF5jK-#N}hq-Whd(DM`FRwTW*zmy3)Al~4AIXtXxRm^^ty6AQzt z3`$&@`+u?RvDJSDIzDO#S|IfPiUWqy9K@%G)L_uSwa}5$s)Gj}_Ax$WeeCwCRm78w z`8CU5U7&7%a9Hve@4Cb4uNy(XM+Sl0|D0>p{Vn}xK>70TKtHl=rAzkOl|sE23r5|1 zymE<(TxYtjhN0?d=10wsLhZgHsZg@T5`7ABPUve8#@-qy4jf^6bhXD}PV0U#20$9kwgO^g z5*<^}k(1gJqV`5rHwa&}y~(QoC~?~MRziYBdjcI~&_s+WF*(tQ;2otR0=Sf58lc|C z&nv_8_oo1;{Q|7YC8AeEE>uqz*z{+AlO#@{HD3mtN%_q@`jYF-y{d6pIOs@h^v$Rp zxf(*T+n>Wa%Be`u2}{h1&n<#$HTl||IJI=SVeC-}Bbec-$<1xQ*G*|)_h2uKO}f~a z1EO{2VdfWqCI|_4=*Vx2WxnP(EoFi^61pQtq5O#(X`6+_>|mMnCc^&>!BaBG#qQQ8 z(;Vs_JF3zJ9fE&y9y<)xHrB8_r}CPs>czXJ!`5d4Z0pzJt_eHG8I zA(5;-1~jd`#ryE*gc8~dZ1m<=O-#$-teB{G@}%ll#lD~qN^|ACF}JEu3F`3*9dYu+Xu2AUN+l9s76E38U|sekid>&1a^F)D z8HHmmV|G6iA-O+2y82%%m*e!6A309pfjRW<3LC>(fl z);`#tA!{p0A|6!E;&&#W;IJ!P*KeQII+eJ&%TSbl0{e&6M1_%Da66Px==|z53tWD> zsO_L`4xASsehs4Y371biW^T|6!zVC$NSRc^SI%HW#fh-03ew}t;3~$TBjz68Bl&q? zg6IX0xpp-Wo+DMRE%uQmoKl3VMi;TY>~5nn@4ahY-|)Hu9?k2ue(^sQm;@9g?1X83 zbygl_KHPgC;1ea(*24QtD>4128>%4uJP8T3HF{cCTykCD?Ildt1t#pH>f4ocV^&!& zgMk9O#piwsQ}2ZY)9izsJ^PaM^8SkEqlGD2$q6LH?pR?G`zvzOBof?j7<&KtHy4?l0Z;}&^?VtZ=gzq1V-qC1+ z(|6WSGhlxK(~`na4Vj~hBw1DIPBrVlRC-z*A`16+#)@Is;87PY!G zQKDK(Jx=PmBl0SM9ETi|?Yr#2E&LI?Ai1ZV%NgNM4b!sY`R-}JNYJV;oye{0$XqWU41`*?!+iLUcg{r@c^e#9rrCV!PAIY$vfq)Z;20sob7w)Ag2Zgeor_c_o7<$3(4%W59Wp1*x0 zC=}W<%k!lDsez=3fO@**eH~0yK+L^%^92v!oMjlObpH{e>mzFOUVA8*w_yT%&|pjb zdsg-YxqV2by@T^mTWi3OFW0E9A*L6*x*VB|M|tXIc~4NVuovo_q;xTyLQlz+4Wselk^h8WEWlmeIP8+FjCxC<8$8ahhR!D(onhZIP_&UVv` zKd%C?p}^AP1~}N(yak8^u@qI+O4zjR31yqNX4w4BtG-P^s{C^ZOhLaM&&qgiHxz&p zRhMCJ@3V@))nTg@u)_d$X8sKWkB90)SD5T|$d3d0qGNkZk8oMZ-aaeCXXKq-`7`KZ zEPNlzbID_SI`NyNLTe=9m>*R64?EV5@8n`|+Q-Cg)A|n&^wLZE&&B-}3=alMR;N-@ zN)CP+lV5#9%;nQ`mykGEmU&WWZ-o3}R_<&rvR{za1tsDU%3C(~04a>kpjvm`h%`A7g*Rg(yMIYwI z5y=Q;rl2S-{c#BBeY)yC<_^3~KBN;3MZShMD1p@J- zXi4t&cRv!dHInL6(f;f;N4%DZnb|uQsLQFnNFcbrJ$IMuM^#VFltXS2!7PvFeGU0xO&R89cwK_0$J9H{dV`*noWYFutsl`L z?omd8uG#w8gIvY{uP$L>Vb#UZ#2EK)MxN{FPV>3(&pV&eDTy%a@ppVu?6?$XfkKkr zTkHIsE_~Cr*rhD?Mcx_z4>UE5ItHUGG<-!F>ccsWE_);_&*z^C8UNebM>YjJ1#0)D zl=t{EWgAHPSDl^VkQ)~qvs)ejk3sNvo#UxtaqVxB#JDn^spfi zGGDvjkS9fR!*Kn?#fKJB=%ZWfncjvoM~CXa3zjqipP@4&Ch+87TFpD#u~oXD%Npu< zXSGe>oCh|gOxuO{`M;yXlOT#Fk209G^-ws3eLhpV{M~rJ!!a1*GVYN0AdS4( z)!!W49s*H-{JF`$1c3jE3`A5@mL>wcR?no=aoP7m^~rn3GJEOAL#9$uU9e=U9Ic99 zf%P==>A!6`a`maMlO9JYoC}3mm-T?2f|}!Ee@^|T6lkzx)hq)> zW8)VdZP*-#dF09S0@q+@v3N_1)M}GPy(NwY#iE5yNW;Mtp>&vA5R(`=eQB|jzWXHu zY2iZgtW>=&cy=nS4sny!iZsxiRsGPKkm79MKK<#_FR!UKj^m;h>V5jpFQZt#Rt<0} zF2cdDsqRmoKheL=d-UVubHFeZh(V89+|v}tB)hB>@mp=LaPcH8-@;<(PyJg|K*B{b zH)8qFVH!?t@(%I8wmj^8us4xC!rC}A)fVqpw(^ZXCg=zF7%tqcqc*Mq&UFZ~-b4z! z8;XvB06Ex!Rl>f~Q3}PPS72*6lLsg47AG#`oo9v1({2u+0YGO8=v|=O5O;icqo5)W zpa|)SYmB_$y3?Oo2Og^Up+9{RxtMW9=Is8I%tJ9Wf7^@Q_FSB8Nps*~))~oK1jME2 zlUOfP3tK#j5dYy;v%03Y>opd_lLpb~laWr63LzOP;(0uv<>vkE-ijy%X(Is^KWa*b zz5UIKf;*1q`1l-fA#UgdmB1Y)o2Rf>_6`es3= z{Ly%$>MegpVfEpYGAAN$+-6N12}=n!Gc$9G5$^Y;resWh{tY=dj$|*R>y-euT;$s~ z8KXAXrX>y+??RhlJ+Hq%n3VPdfW*0q-&x!eceeT~r!L<0V^$s4}DM4K-Qzm?BM zp-K0_z`JlT)17Lb813+wiigDgxj}Ch_!&!PGfHeAmB+mPh_XbH>w#-!W*sK)3_yGt z(x$msu{~1|liM4!dhW(a#S zy@v#i3QNEb$J4J`%~GL$0unIs0D_**Ob>tJ5Q^J8}XT|!5-s?nE+*NP9_hyYhpAXQ{T=xaWe~9!5o59RI zl#XEe)|f+b9BpH>dW3xZp-cg$=K>A(Gs%g?0tOdd=8i^ic5Rw-1%|j#%LiUAIn_k= zuUHCxph8F}t40r=hk9ERDRpk&%Xco$=XTBkHd8kEobdG<2nT(j>$5t^-wy2q-(Mb3 z*L5CjQGR?1QcIP@uU@}qcGv}vz?+!UvPEYIggmzFaZsZ+2$}ZBo74nu)E&3ey?_7n zWS01#y$EG!$f8yDGt+x7wQOrF^j7n^wyG4m z1k5=&*KMMIqIfpLQU0-+MM|L%h+96JlDG3;siPzHZHvA-mV__Pz`*3ceXCpU^Yi*6JI5&&3ulR_VHsNf--}4J4#$I0K&7fMq z%>Q53mq^iO7DL5(i_3dXpd)7Pt;QZW^(JD3cXMO+*Ag?nVRcF-V;;?c$h<0|+O!Si zb1j;6$%>Ma`h-MV?%oyA`Fxyh4wNtMhvOe}>KJ=Ew~Z1*5_txg45H5!k+Qh@l!V}X zv!#K7@lUUf95T5PA^u$sWvTS`V`+? zveSqY3iO$qo0FX|4lO!>06L$X7?TZs7Ihf5XNSR2VE*pBiNeRA*B3LAk{Y0s?Pr~q zn_}pEd=#HBZ$lfzeRV_6vW&#CJc3NCqXT1#jw?9PpkAX#&ACHg8!_PRvwigV`TUXH zrz|&Q1MZMgA03hOkBz{_I`UX6ks9jT+GbGk0}x5<`m`{R_g%1bNkNum;s;MZzi?Nk z@vv>#y7r~XXh+pZVWK?s;nx#hr%WXXeW`_N&3N%ebOP)W3PQR7KR^En*4-1%K(Y<> z85=mAHdiXOzT zp%=?HqC}UacB8B_mh+U9xF%(OA4Pw6TdE=>wk@JfeN5bI>|KW7L&tAaOlR#!!|n*E zE0Ib+JebRAjapDHjY@HoT5D76fne6ExjS*rJH%QkSl3YJwVX%{p zH`=7M(DLOo@7}((gbXDn5a$k^Y_Iy^FyT`UJumjafEx=3qt~x)waV-?*%nWK7Eb1a7_p2H;Avfh%y)%b<@}ON{eAh37E!Dp z9@C^;d~VULUj9hBxHX0C*|TS(58p!Pad@i9M?Ze`kDF=-`+I(eM<;iLIwZMf9+0rm zt+$*%hi|R3e&rqMg0yI0<-YU*FN zc<1v12Q-3V(g@h@haV*a$8-U7BuGNBle=116*5zO6$;Y~J%`rS>E_(zVkm@o|Sx6vAg(lYp7oPNTEq%V}*T)W%x32E|h3?tB zw>x8Ppdfz``+>=h(aZvLfMiF@FZ(_Ec6rs+)f$=55Z=o6Xilh8%qW9GZ1E6TBSGN$rYy&*f71i+UZOv0v}(E}&2en1OG1 zfT`&Li$QC=7B{;V4F}|1Mw%GlJPLKp+G>fLrJ%yw!aDop@75e znPs^fRBi4T4X9oNnS55~EwmVwJl`;y2lI(*=-{#~C~FVoNzaL(Gunup*m=+o+&22n zi361p>-A2}$$gp)ZlKhfV5lNpR#ycn99V-s2|JZ_V+(s&eZmLo85Fg8(lI|iE+}v6 zVLUnQ`olcwAO!-jr>vd0j=N^@CUm)*?_z2zra!+k;10~q&Go6_)rw=UfgnRaom?(0cZg&oS_r$#uy2H9~|r zF7l&T5|SH$JINQh74YPRJ|~K0Ev9Jv`Sroc8@LiA$&=l6_(|_X1N+4N?i(h&`{WxD zC67h>IbD5_J*j9BW>8y8OH0szkM>|J^qhN)K2?q#WZJ>WPi;Yb!XWCVbQR_1`RCv2 zE3h-Jd?;$u*S1h`;57@45#}fR`CgufdCFNC+)(5r(Pb7qPHkTw2y9hs3|MbKk<xh^>YkGVvf^esT92FRzB zJPd9Fx6=bXj*DNKVx)0iJGkM%1j;RUAiko}aaME_tY%5t=~1|AV*q(5iZM?G+gdk` zYSphg0SMtLzOBlLHwuXn%7(h9TsRL03cZ7V`nPr>@dQ3h^<`1Mgb+zXcLX}e(tW96 zQsY@?LA2Rcvz^!7M;mh#7muKw+vryYt|;I{-SSXWOobR0B8(>O6bZnuFG%sg^iEL> zb1wc-#&4^g2*{dH9^wqX9zs*JiRD}OwrI`E>#JB;Q_Idb^29oAR;x`M4v;ka%w7e> z$lwydcvq_Bl-jr@1@;fmi<0R4_=LACVZBirBylRnJwEAG_w*x}3=nuzVjQ3E_Uzfh zYXzQSV|Tj-)p*VT*4zodlq0oA{zZ9hMdG_h8qhEz!=NI5K3V%4rkH3+y; z7^whdqV!PJNFz`UPT(P4P%Tly+q=}ObpTQ?0v9k1pi!5YVUw;X@4ri2E!?lm*-tb8C33hopF zA0tdZ)1Q+ly)%~+GucU0%*S0@k}QmE=>!c+BT*w_bOUEsu)FIq-022&EewqHirGW6 zjt*F7I)~1V=7$OvHy|Z8_>8EEG4h2!NoTyl>j|rU|308opa(_@EH6E&XM1!u0mx~3 zi|fyzM_^0(JpH#sO*^G1rUn z6xBh&I#tp%Vq`&>ekati`|h+vPG8GA@6L98zc8Oak+Wb^lDNj#TPP9 z-^Iv}7JRF5t+yC1FN5RinzR~WE7g8DlbZ#e@OKb8Z^X(BJ!O+V)?*2z@X!1+ooEDrD+!XkUdA^r6fA{Fu6(geCV-Am4|i ziy=Dv;lNBNQPNJAvDt(^RUfuq7FJt(0QbAiYU+jWehG+f%E<@Zq4MSS1R2m4NFU-7 z?(l!S00mG~oLz8$rqwKCN>jxZf82tG;$l))auT{%HBOp5vpUkz>8Pz?6nc7Sy#%&p{iQuyj&o7{!^EbFhr+i3=pzhA)%$l<-VssjO|gP*;k+=|&tMpkO1ro^ISGG$|=oOX$3qPj!R-USuQq@fi^FRm;y@kUZ zk)257gXXBxt|Ex*zw+sW&5j4d&^8>H`0X$Qxyz-b0)yXmx>*u1VaLR>5xruKtAy|R zR#64yiyYvM`{(*|Fwn2resdJ}^SHRJO}NyGN*lYPkx|U0L_S-9DZD9;uhB|@AjkKC z&)C3?eE^E|8_X6{or&R=B{Tt+3St-x*qJGJAf6g92HL0klN_&r`7sy3+1y!r)0VP0 zsaoZ>XqorDwE5c#Vim)8GU?5dl3ZWS;?=YGvMOP$v7$^>CQ20}5-&h=!FKQ{ zbJ*aZK?#hCxUnkfzLuol*4)(ORB^Bz?p$3j^dA?6i@?}iSO@2apYQKO)DcSJ6yiIavkY*9K68u#Y!>ga2}SJ ztavkwsbC^?c_MXICy(g&%)xk^*S74sW~XOQYvZPoXScHFgv&@JA+zf3B8PivpBL(q zbuwelsKik+Wg}xB-<9NtwMA>ODlO6n95tQZ7z#z)J2(ZU!#ou__uOj^iPx@0m63Mt zXb+2d<-b^3-DBLO;Mt-e7v|bJWa^G@e-RMoy-i- zTA(E&|M=NQfs^vbpu=|o+wX_=F6pkWmPJNAAFi;)stqo9DO4}M1ENyj*O>fTOz}#!Ax3l)?n^fs_uXim35c z_h?k^C%}jptt#;=d`*GM{0kYLGGdp)(8vc^ry@mutPvDEQcZDzp3d2LK_?L3x`G#;Sm5 zpW(&C2;xubWWLmAoN|h^@LhErC)Jh!uH3ToG;FVEPhetfL~cDBrZG59^nj+#g$~h9 zz>cdA1i4~N)#9c`16CuASU!-UWYjD+)l*4j@_H@nmp_W0;eR;6T}3eEkF;i=<_S1>jC>`HJ1o)r)f80SujRiIXrUVn*zVKeWG@Z@JGoFHN&#HZ#PcruS;y^?vR z4<1tpZxtl);CnPaa&$-<83e1`35U9w0{c;gM^o`ZHh>U0b~*_`C|8aHrE{YK9$61G zRSpmHc$K?qD4);f=Wd{Rdt8|VA2=oF!d3zgomN=b2P(Eg3yf1J0)=m{8EGzf8qWb2 zP6@oFK5)_)&{=vaybmI1ARGy?YJx*F!gHEN&xc*n;*|Rz)?`s7d|*0I>>Qb#=gzhM zMM=Qa0UM}}{=N&o7&(Ly7^%d%v@0>+wzaiwpjzA6qjO-t=)o@+ffYxVz`x&#+%N+koU(PhSl?cL@(dQ5=Kbpf@kNpwzhTy)#^^!U!Sk$ z!%l2Rj&I=;U|-TofX}HLXrS?Rwo5KeOo9d$;WFDvn88A!zu)i*71)KV5qWF&M_I|I zPoDxx*(-*5ILHzd&j7bwdIo>M(YVGQ{k0xCu=>#Vw_u5DehUs&V(%uoDZQ?x6A}*~ zxLi;=dXd(cxMTVe5(X6UJPz0iAEPS)vz!IGqSrYS^hvMT?q)}Ht>F5MMg5Nn1ZXz4BYM_tk**` zAz*H83tz7T3qkN3oWm}qp)wE_maA#fUJI|kzCgExm1Iep|xNnq#Aaxi(l% z%y$qdnrVHDktrauqoc!PJyCNGo_=l1^Jn;144;o2tn_>L?gf<62{^O>XNRnKxP1{6 zhXpx%gc}hhXRhtgz?G*bEP<)X&&+y0yojwW8F(yo%9on)r;Gz5zAZ&SB*Xv$cEQ}- zT*Rpv%Xttgqf$g*=1L`$=S(X{k*L;B0Y2P5WQK0ac^K|ghc=|>-u2p746B)aMnz*5 zJoxk!J|c#Y1W?gGtCVp8SI~-74$wK~iaz$&>iMQ77i)+%E(BRH9WtbLsN?Z{4IWHW>i@Io3P=uJ1@h4@~9;6xrNGby^D?`CAX67@5wtlLlsK} z35$q$Vz}dH_~1CHmF|MMOy2btl?aaY1xX#eX@#8zFb1WS-V}BBVVJo9;3-BL8fAdC z5M=g`gYVcyt^IClZ2V%pzw+8;G=NzTs#e7qQ5ZMZP&Gv(0)av3gBd&i*z@?Tx~{GT zs6NhWZabFXEz1T6PCqz)Y)~lFXP$CPa(0?WglJWy9S*E&t)z#JA(LWOo$1i(l5nOH zERo+cYKhtO><#KJJ<$~Sw?ZQddczj0siDMLcIE~HP#zdOpaSj$*-J(QZH?U39Eq0m zUuDr>fx~I)4lIrhu`S5`hQaaoo1@CNJCkit;POfrH&?`$G-*^EuwY{>qNL(8Su6k< zw)K8%3_g0%b@BBQvuO4TT^vHk(mJO|1YS7C>gVDJm*5c`x<6 z6{E`&=ctP8MgM2=_&;3(EvM!hr1@U{Cam59q{8jVFnpI{V*tklQy@Ddqj-j|1eS5l zMVaTVmVbRPJfGT^FRK3V!MEZ-p#_>Asd`qNfAido&O-DIAlUD(PPQU6N#~0(Q4$(n zfw*rx{r&xjL+R~E)9Qpp)-)fDQ^jQ8#|mCNEidLDjes4kRTLZ z0jfu^TjbGDMA=e>AcUWZ9R+RUq@avWRZ=@Sgsv;$ckzEV4LOHBj7RGI0EZfsr$bb? z%`tDyM+T7fWEwB-NL=KMXd+72%gaXmC@x?^(*qUJ!}uCktkAGu=NAK5S1+k9@!)ss zsBmMes})k=+^kQDi9yc<R2sz#yJ{^KBy46du!Ku86m9r}Iq^*EALUjOHf4pkO|@ zar6M;NCdTuSHUX#FDfZU#Sy3$@}YCx8YoX)W~cE6TAfIF{hMd?avQWW3@jlI9I~dq zcq+nz2;Ox*jL_BvT6vI(j`BRXlKjP*VLjXieHh@cK-$l#=q*?SbO&rgiugktC086z z1Du#@2le}**%StgwilBymn9#%iD6FSs_KEVBKuns2JNZt)Ypa{%U^q7#-dI#*C2r+ zVEAsZu+CBPG>*TVapZ1@m{}yqIEgp5#SM}mI|(5UcXss}JP$zh&Zh?(J>`zuVYxUd z5~Lv`KU@MLc+KGzi}Gh)M&EwwNt!#s%_OY;w7`2Wgd_=cFg?pZH4R<~%Z3EVCk22I z8~$$kd}wmAm5}^pfE-vO_NJ6&vvXQ5eupM8hK)0!j~^R!2SR^Tfah5Un3AZXa-fv|`qZnlR@~mHr+4P;COKe5= z%X0)mjnfBX0x-~Tk#Ey4d%-qv&v(G=jF8C$GutX1vJppOyp~d*&*#Zb;n(V#=ikCeYf3XAl`K9f^fgk-a-jc(T z3d~*UM4B*|?{6WL&{wi!L|ZM@x3f>@F$7r^Oy$gnbxsYUDaQWH%_u(xB44aHo=Br#;-u z3`tCOthXG51DHkGx|uze6Sa7tY?!wrGz*Lz=vh74IXUnOrA^-e2GN58(U3e5!Yyrh4VKXu;P=;xdIJu;*C2;tYTKYm zX^ViWV;OCQqX;)6mDwl>MmpA# z2LuFU5c%algZowXVfQ=9y^SC3vqz8+QJ#Vc(z~wb9>5{<7hu>^!FnySJ)kZ8Dtjfv zDO!uZta#_eA43?{_Eep7unR)W-e}abEHa3r8F7I(ROW;7LD9-aiJ+^lP)Si*a5n`*K4chB$9^`%4yIW z67FjjP1T~e)Pf10Ys1s*_;K+xeqWi7oIN2%GCq4BWY24=Nzi?89}aKIhHAS6!-I@B z&@Zrcp6mk%qS{i${)IV7Gw~_T9XftaBZ0f%-erhUX;3V_di9Fw1-%#aTP&OyQ1A_u z%S9qA81yRYNdUe|4$2*0 zs-)cm;QQ|GkCSixlS2H>%xH^Xk@ zHIoD4cElzmaCU~rjD$}xkqp02@;yW;!rPVQGOx#M)dvGUEaMF(wNZp*o@!sITg6p8=PzdR;Spv3}g~h-_$MsxXTm}mq)F+;R0V>t7Bp-JX*4;x2xTtdvTSEWv=IO|J zyW-ffDO_M*tWdq5X7AUcZ(uOzwFngp+NN!oQk-`#%XwCt*{97KWS9ru{A+P{=|3eG z`&Uy?av&`kz~@fv+je{mp*q8x7FX%RCpaax_dX_O!E3fJ+X5UtPTeONKC+jpGmS;p zgcHb<%3fQO|6e%%FJPUSPQ7qss{KR8W4VW7&5HUbr(`Wn7}|OIBc@O--ZBg!rkJ zCqTf!`n_b`;{T^>6rxkfN9~)vvwLDoU&BuG46Y)$PXa&OgBMD76{!n8XH_DleE=ES zwe7%?pK7nCB9GONj}@7hvwmxW4AU8h5nS`WXoM4+Qm&s?u19AY?cuyG8g-xg>u=4b z3UhNsU8B8bhi5bakh+%chX)c6?L%$yNBaAp8>_ty87dmfg5N0n&l=o?V6$YWr1pn= z@^^(AoC8lof>=cA53l5JT(`x{-Y_!!IOY#1?{A<$V-9QR0qOleCAPmWWC`|CRRyE; zA9DZS!GOI5a^@Iy)6#!+AtL~n{Ji`6Rh$gqvV_Gx}d$NM!{?s{`#dya9mN znLCt#fN19GWqGZCcmb>g!V!v`uld8i{hMI*@rFmapFS1x&x*hc#U3!RH_({h2~_@P zMf~%n|FOV9bhTamcKZML!~c5?jh%&v*HTH1x6$B#FYf;@^8YJ${Xe|O|5; + /// 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 @@ - - - - - - -