From f1a57b593b34a8c46e93fec2203cf6fdf5e13092 Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Wed, 19 Mar 2025 21:41:44 -0400 Subject: [PATCH] project cleanup; file reorg; rewrite README.md; --- DVMConsole.sln | 4 +- DVMConsole/AliasTools.cs | 59 +- DVMConsole/AmbeNative.cs | 6 +- DVMConsole/App.xaml | 2 +- DVMConsole/App.xaml.cs | 23 +- DVMConsole/AssemblyInfo.cs | 13 + DVMConsole/Assets/WhackerLinkLogoV4.png | Bin 90042 -> 0 bytes DVMConsole/AudioConverter.cs | 6 +- DVMConsole/AudioManager.cs | 75 +-- DVMConsole/AudioSettingsWindow.xaml | 2 +- DVMConsole/AudioSettingsWindow.xaml.cs | 89 ++- DVMConsole/CallHistoryWindow.xaml | 4 +- DVMConsole/CallHistoryWindow.xaml.cs | 136 ++++- DVMConsole/ChannelPosition.cs | 21 - DVMConsole/Codeplug.cs | 152 ++++- DVMConsole/ConsoleNative.cs | 29 - DVMConsole/DVMConsole.csproj | 9 +- DVMConsole/DigitalPageWindow.xaml | 4 +- DVMConsole/DigitalPageWindow.xaml.cs | 21 +- DVMConsole/FlashingBackgroundManager.cs | 137 +++-- DVMConsole/FneSystemBase.DMR.cs | 6 +- DVMConsole/FneSystemBase.NXDN.cs | 6 +- DVMConsole/FneSystemBase.P25.cs | 66 +- DVMConsole/FneSystemBase.cs | 13 +- DVMConsole/FneSystemManager.cs | 46 +- DVMConsole/GainSampleProvider.cs | 66 +- DVMConsole/KeyStatusWindow.xaml | 2 +- DVMConsole/KeyStatusWindow.xaml.cs | 83 ++- DVMConsole/MBEToneDetector.cs | 31 +- DVMConsole/MainWindow.xaml | 6 +- DVMConsole/MainWindow.xaml.cs | 577 ++++++++++++------ DVMConsole/P25Crypto.cs | 357 ++++++----- DVMConsole/PeerSystem.cs | 8 +- DVMConsole/QuickCallPage.xaml | 4 +- DVMConsole/QuickCallPage.xaml.cs | 41 +- DVMConsole/SampleTimeConvert.cs | 6 +- DVMConsole/SelectedChannelsManager.cs | 59 +- DVMConsole/SettingsManager.cs | 82 ++- DVMConsole/ToneGenerator.cs | 48 +- DVMConsole/VocoderInterop.cs | 194 +++--- DVMConsole/VocoderToneLookupTable.cs | 19 +- DVMConsole/WaveFilePlaybackManager.cs | 98 ++- DVMConsole/WidgetSelectionWindow.xaml | 2 +- DVMConsole/WidgetSelectionWindow.xaml.cs | 38 +- DVMConsole/codeplugs/codeplug.yml | 4 +- README.md | 14 +- .../Controls}/AlertTone.xaml | 7 +- .../Controls}/AlertTone.xaml.cs | 105 ++-- .../Controls}/ChannelBox.xaml | 10 +- .../Controls}/ChannelBox.xaml.cs | 260 ++++++-- .../Controls}/SystemStatusBox.xaml | 5 +- .../Controls}/SystemStatusBox.xaml.cs | 63 +- 52 files changed, 2028 insertions(+), 1090 deletions(-) delete mode 100644 DVMConsole/Assets/WhackerLinkLogoV4.png delete mode 100644 DVMConsole/ChannelPosition.cs delete mode 100644 DVMConsole/ConsoleNative.cs rename {DVMConsole => dvmconsole/Controls}/AlertTone.xaml (92%) rename {DVMConsole => dvmconsole/Controls}/AlertTone.xaml.cs (53%) rename {DVMConsole => dvmconsole/Controls}/ChannelBox.xaml (95%) rename {DVMConsole => dvmconsole/Controls}/ChannelBox.xaml.cs (62%) rename {DVMConsole => dvmconsole/Controls}/SystemStatusBox.xaml (89%) rename {DVMConsole => dvmconsole/Controls}/SystemStatusBox.xaml.cs (50%) diff --git a/DVMConsole.sln b/DVMConsole.sln index 38bd846..ad4ef8c 100644 --- a/DVMConsole.sln +++ b/DVMConsole.sln @@ -3,14 +3,14 @@ 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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "fnecore", "fnecore\fnecore.csproj", "{1F06ECB1-9928-1430-63F4-2E01522A0510}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/DVMConsole/AliasTools.cs b/DVMConsole/AliasTools.cs index 6428c8f..064a0bd 100644 --- a/DVMConsole/AliasTools.cs +++ b/DVMConsole/AliasTools.cs @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only /** -* Digital Voice Modem - DVMConsole +* Digital Voice Modem - Desktop Dispatch Console * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * -* @package DVM / DVM Console +* @package DVM / Desktop Dispatch Console * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) * * Copyright (C) 2025 Caleb, K4PHP @@ -12,23 +12,46 @@ */ using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using YamlDotNet.Serialization.NamingConventions; + using YamlDotNet.Serialization; -using System.Diagnostics; +using YamlDotNet.Serialization.NamingConventions; -namespace DVMConsole +namespace dvmconsole { + /// + /// + /// + public class RadioAlias + { + /// + /// + /// + public string Alias { get; set; } + /// + /// + /// + public int Rid { get; set; } + } //public class RadioAlias + + /// + /// + /// public static class AliasTools { + /* + ** Methods + */ + + /// + /// + /// + /// + /// + /// public static List LoadAliases(string filePath) { if (!File.Exists(filePath)) - { throw new FileNotFoundException("Alias file not found.", filePath); - } var deserializer = new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) @@ -38,6 +61,12 @@ namespace DVMConsole return deserializer.Deserialize>(yamlText); } + /// + /// + /// + /// + /// + /// public static string GetAliasByRid(List aliases, int rid) { if (aliases == null || aliases.Count == 0) @@ -46,11 +75,5 @@ namespace DVMConsole var match = aliases.FirstOrDefault(a => a.Rid == rid); return match?.Alias ?? string.Empty; } - } - - public class RadioAlias - { - public string Alias { get; set; } - public int Rid { get; set; } - } -} + } //public static class AliasTools +} // namespace DVMConsole diff --git a/DVMConsole/AmbeNative.cs b/DVMConsole/AmbeNative.cs index b9575a6..a983ebd 100644 --- a/DVMConsole/AmbeNative.cs +++ b/DVMConsole/AmbeNative.cs @@ -11,8 +11,10 @@ * */ +using System.Runtime.InteropServices; + #if WIN32 -namespace DVMConsole +namespace dvmconsole { /// /// Implements P/Invoke to callback into external AMBE encoder/decoder library. @@ -447,4 +449,4 @@ namespace DVMConsole private static extern uint ambe_voice_enc([Out] IntPtr codeword, [In] short bitSteal, [In] IntPtr samples, [In] short sampleLength, [In] ushort cmode, [In] short n, [In] short unk, [In] IntPtr state); } // public class AmbeVocoder } -#endif \ No newline at end of file +#endif diff --git a/DVMConsole/App.xaml b/DVMConsole/App.xaml index 6263b34..c1064e9 100644 --- a/DVMConsole/App.xaml +++ b/DVMConsole/App.xaml @@ -1,4 +1,4 @@ - diff --git a/DVMConsole/App.xaml.cs b/DVMConsole/App.xaml.cs index 9facf73..a9a5e37 100644 --- a/DVMConsole/App.xaml.cs +++ b/DVMConsole/App.xaml.cs @@ -1,14 +1,25 @@ -using System.Configuration; -using System.Data; +// SPDX-License-Identifier: AGPL-3.0-only +/** +* Digital Voice Modem - Desktop Dispatch Console +* AGPLv3 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Desktop Dispatch Console +* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) +* +* Copyright (C) 2025 Caleb, K4PHP +* +*/ + using System.Windows; -namespace DVMConsole +namespace dvmconsole { /// - /// Interaction logic for App.xaml + /// /// public partial class App : Application { + /* stub */ } - -} +} // namespace dvmconsole diff --git a/DVMConsole/AssemblyInfo.cs b/DVMConsole/AssemblyInfo.cs index b0ec827..e2f2862 100644 --- a/DVMConsole/AssemblyInfo.cs +++ b/DVMConsole/AssemblyInfo.cs @@ -1,3 +1,16 @@ +// SPDX-License-Identifier: AGPL-3.0-only +/** +* Digital Voice Modem - Desktop Dispatch Console +* AGPLv3 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Desktop Dispatch Console +* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) +* +* Copyright (C) 2025 Caleb, K4PHP +* +*/ + using System.Windows; [assembly: ThemeInfo( diff --git a/DVMConsole/Assets/WhackerLinkLogoV4.png b/DVMConsole/Assets/WhackerLinkLogoV4.png deleted file mode 100644 index 683592a786335edbb50fdf995859889ba099313e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90042 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∋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 z

7>i0V(yfZ$!;=HoB+lWvW=4PH)x#>7%xtqG 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 diff --git a/DVMConsole/AudioConverter.cs b/DVMConsole/AudioConverter.cs index 021fc31..c4db6f5 100644 --- a/DVMConsole/AudioConverter.cs +++ b/DVMConsole/AudioConverter.cs @@ -1,17 +1,17 @@ // SPDX-License-Identifier: AGPL-3.0-only /** -* Digital Voice Modem - DVMConsole +* Digital Voice Modem - Desktop Dispatch Console * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * -* @package DVM / DVM Console +* @package DVM / Desktop Dispatch Console * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) * * Copyright (C) 2025 Caleb, K4PHP * */ -namespace DVMConsole +namespace dvmconsole { ///

/// Helper to convert audio between different chunk sizes diff --git a/DVMConsole/AudioManager.cs b/DVMConsole/AudioManager.cs index c925342..eb442a7 100644 --- a/DVMConsole/AudioManager.cs +++ b/DVMConsole/AudioManager.cs @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only /** -* Digital Voice Modem - DVMConsole +* Digital Voice Modem - Desktop Dispatch Console * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * -* @package DVM / DVM Console +* @package DVM / Desktop Dispatch Console * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) * * Copyright (C) 2025 Caleb, K4PHP @@ -14,23 +14,27 @@ using NAudio.Wave; using NAudio.Wave.SampleProviders; -namespace DVMConsole +namespace dvmconsole { /// - /// Class for managing audio streams + /// Class for managing audio streams. /// public class AudioManager { - private Dictionary _talkgroupProviders; - private SettingsManager _settingsManager; + private Dictionary talkgroupProviders; + private SettingsManager settingsManager; + + /* + ** Methods + */ /// - /// Creates an instance of + /// Creates an instance of class. /// public AudioManager(SettingsManager settingsManager) { - _settingsManager = settingsManager; - _talkgroupProviders = new Dictionary(); + this.settingsManager = settingsManager; + talkgroupProviders = new Dictionary(); } /// @@ -40,10 +44,10 @@ namespace DVMConsole /// public void AddTalkgroupStream(string talkgroupId, byte[] audioData) { - if (!_talkgroupProviders.ContainsKey(talkgroupId)) + if (!talkgroupProviders.ContainsKey(talkgroupId)) AddTalkgroupStream(talkgroupId); - _talkgroupProviders[talkgroupId].buffer.AddSamples(audioData, 0, audioData.Length); + talkgroupProviders[talkgroupId].buffer.AddSamples(audioData, 0, audioData.Length); } /// @@ -52,34 +56,19 @@ namespace DVMConsole /// private void AddTalkgroupStream(string talkgroupId) { - int deviceIndex = _settingsManager.ChannelOutputDevices.ContainsKey(talkgroupId) ? _settingsManager.ChannelOutputDevices[talkgroupId] : 0; - - var waveOut = new WaveOutEvent - { - DeviceNumber = deviceIndex - }; - - var bufferProvider = new BufferedWaveProvider(new WaveFormat(8000, 16, 1)) - { - DiscardOnBufferOverflow = true - }; + int deviceIndex = settingsManager.ChannelOutputDevices.ContainsKey(talkgroupId) ? settingsManager.ChannelOutputDevices[talkgroupId] : 0; - var gainProvider = new GainSampleProvider(bufferProvider.ToSampleProvider()) - { - Gain = 1.0f - }; - - var mixer = new MixingSampleProvider(WaveFormat.CreateIeeeFloatWaveFormat(8000, 1)) - { - ReadFully = true - }; + var waveOut = new WaveOutEvent { DeviceNumber = deviceIndex }; + var bufferProvider = new BufferedWaveProvider(new WaveFormat(8000, 16, 1)) { DiscardOnBufferOverflow = true }; + var gainProvider = new GainSampleProvider(bufferProvider.ToSampleProvider()) { Gain = 1.0f }; + var mixer = new MixingSampleProvider(WaveFormat.CreateIeeeFloatWaveFormat(8000, 1)) { ReadFully = true }; mixer.AddMixerInput(gainProvider); waveOut.Init(mixer); waveOut.Play(); - _talkgroupProviders[talkgroupId] = (waveOut, mixer, bufferProvider, gainProvider); + talkgroupProviders[talkgroupId] = (waveOut, mixer, bufferProvider, gainProvider); } /// @@ -87,14 +76,12 @@ namespace DVMConsole /// public void SetTalkgroupVolume(string talkgroupId, float volume) { - if (_talkgroupProviders.ContainsKey(talkgroupId)) - { - _talkgroupProviders[talkgroupId].gainProvider.Gain = volume; - } + if (talkgroupProviders.ContainsKey(talkgroupId)) + talkgroupProviders[talkgroupId].gainProvider.Gain = volume; else { AddTalkgroupStream(talkgroupId); - _talkgroupProviders[talkgroupId].gainProvider.Gain = volume; + talkgroupProviders[talkgroupId].gainProvider.Gain = volume; } } @@ -105,13 +92,13 @@ namespace DVMConsole /// public void SetTalkgroupOutputDevice(string talkgroupId, int deviceIndex) { - if (_talkgroupProviders.ContainsKey(talkgroupId)) + if (talkgroupProviders.ContainsKey(talkgroupId)) { - _talkgroupProviders[talkgroupId].waveOut.Stop(); - _talkgroupProviders.Remove(talkgroupId); + talkgroupProviders[talkgroupId].waveOut.Stop(); + talkgroupProviders.Remove(talkgroupId); } - _settingsManager.UpdateChannelOutputDevice(talkgroupId, deviceIndex); + settingsManager.UpdateChannelOutputDevice(talkgroupId, deviceIndex); AddTalkgroupStream(talkgroupId); } @@ -120,8 +107,8 @@ namespace DVMConsole /// public void Stop() { - foreach (var provider in _talkgroupProviders.Values) + foreach (var provider in talkgroupProviders.Values) provider.waveOut.Stop(); } - } -} + } // public class AudioManager +} // namespace dvmconsole diff --git a/DVMConsole/AudioSettingsWindow.xaml b/DVMConsole/AudioSettingsWindow.xaml index 6f5c59e..cdff37c 100644 --- a/DVMConsole/AudioSettingsWindow.xaml +++ b/DVMConsole/AudioSettingsWindow.xaml @@ -1,4 +1,4 @@ - + /// Interaction logic for AudioSettingsWindow.xaml. + /// public partial class AudioSettingsWindow : Window { - private readonly SettingsManager _settingsManager; - private readonly AudioManager _audioManager; - private readonly List _channels; - private readonly Dictionary _selectedOutputDevices = new Dictionary(); - + private readonly SettingsManager settingsManager; + private readonly AudioManager audioManager; + private readonly List channels; + private readonly Dictionary selectedOutputDevices = new Dictionary(); + + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// public AudioSettingsWindow(SettingsManager settingsManager, AudioManager audioManager, List channels) { InitializeComponent(); - _settingsManager = settingsManager; - _audioManager = audioManager; - _channels = channels; + this.settingsManager = settingsManager; + this.audioManager = audioManager; + this.channels = channels; LoadAudioDevices(); LoadChannelOutputSettings(); } + /// + /// + /// private void LoadAudioDevices() { List inputDevices = GetAudioInputDevices(); List outputDevices = GetAudioOutputDevices(); InputDeviceComboBox.ItemsSource = inputDevices; - InputDeviceComboBox.SelectedIndex = _settingsManager.ChannelOutputDevices.ContainsKey("GLOBAL_INPUT") - ? _settingsManager.ChannelOutputDevices["GLOBAL_INPUT"] - : 0; + InputDeviceComboBox.SelectedIndex = settingsManager.ChannelOutputDevices.ContainsKey("GLOBAL_INPUT") + ? settingsManager.ChannelOutputDevices["GLOBAL_INPUT"] : 0; } + /// + /// + /// private void LoadChannelOutputSettings() { List outputDevices = GetAudioOutputDevices(); - foreach (var channel in _channels) + foreach (var channel in channels) { TextBlock channelLabel = new TextBlock { @@ -65,15 +82,15 @@ namespace DVMConsole { Width = 350, ItemsSource = outputDevices, - SelectedIndex = _settingsManager.ChannelOutputDevices.ContainsKey(channel.Tgid) - ? _settingsManager.ChannelOutputDevices[channel.Tgid] + SelectedIndex = settingsManager.ChannelOutputDevices.ContainsKey(channel.Tgid) + ? settingsManager.ChannelOutputDevices[channel.Tgid] : 0 }; outputDeviceComboBox.SelectionChanged += (s, e) => { int selectedIndex = outputDeviceComboBox.SelectedIndex; - _selectedOutputDevices[channel.Tgid] = selectedIndex; + selectedOutputDevices[channel.Tgid] = selectedIndex; }; ChannelOutputStackPanel.Children.Add(channelLabel); @@ -81,6 +98,10 @@ namespace DVMConsole } } + /// + /// + /// + /// private List GetAudioInputDevices() { List inputDevices = new List(); @@ -94,6 +115,10 @@ namespace DVMConsole return inputDevices; } + /// + /// + /// + /// private List GetAudioOutputDevices() { List outputDevices = new List(); @@ -107,25 +132,35 @@ namespace DVMConsole return outputDevices; } + /// + /// + /// + /// + /// private void SaveButton_Click(object sender, RoutedEventArgs e) { int selectedInputIndex = InputDeviceComboBox.SelectedIndex; - _settingsManager.UpdateChannelOutputDevice("GLOBAL_INPUT", selectedInputIndex); + settingsManager.UpdateChannelOutputDevice("GLOBAL_INPUT", selectedInputIndex); - foreach (var entry in _selectedOutputDevices) + foreach (var entry in selectedOutputDevices) { - _settingsManager.UpdateChannelOutputDevice(entry.Key, entry.Value); - _audioManager.SetTalkgroupOutputDevice(entry.Key, entry.Value); + settingsManager.UpdateChannelOutputDevice(entry.Key, entry.Value); + audioManager.SetTalkgroupOutputDevice(entry.Key, entry.Value); } DialogResult = true; Close(); } + /// + /// + /// + /// + /// private void CancelButton_Click(object sender, RoutedEventArgs e) { DialogResult = false; Close(); } - } -} + } // public partial class AudioSettingsWindow : Window +} // namespace dvmconsole diff --git a/DVMConsole/CallHistoryWindow.xaml b/DVMConsole/CallHistoryWindow.xaml index e593803..a58a974 100644 --- a/DVMConsole/CallHistoryWindow.xaml +++ b/DVMConsole/CallHistoryWindow.xaml @@ -1,9 +1,9 @@ - diff --git a/DVMConsole/CallHistoryWindow.xaml.cs b/DVMConsole/CallHistoryWindow.xaml.cs index e8566a6..8cb361b 100644 --- a/DVMConsole/CallHistoryWindow.xaml.cs +++ b/DVMConsole/CallHistoryWindow.xaml.cs @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only /** -* Digital Voice Modem - DVMConsole +* Digital Voice Modem - Desktop Dispatch Console * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * -* @package DVM / DVM Console +* @package DVM / Desktop Dispatch Console * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) * * Copyright (C) 2025 Caleb, K4PHP @@ -15,12 +15,91 @@ using System.Collections.ObjectModel; using System.Windows; using System.Windows.Media; -namespace DVMConsole +namespace dvmconsole { + /// + /// + /// + public class CallHistoryViewModel + { + /* + ** Properties + */ + + /// + /// + /// + public ObservableCollection CallHistory { get; set; } + + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// + public CallHistoryViewModel() + { + CallHistory = new ObservableCollection(); + } + } // public class CallHistoryViewModel + + /// + /// + /// + public class CallEntry : DependencyObject + { + public static readonly DependencyProperty BackgroundColorProperty = + DependencyProperty.Register(nameof(BackgroundColor), typeof(Brush), typeof(CallEntry), new PropertyMetadata(Brushes.Transparent)); + + /* + ** Properties + */ + + /// + /// + /// + public string Channel { get; set; } + /// + /// + /// + public int SrcId { get; set; } + /// + /// + /// + public int DstId { get; set; } + + /// + /// + /// + public Brush BackgroundColor + { + get { return (Brush)GetValue(BackgroundColorProperty); } + set { SetValue(BackgroundColorProperty, value); } + } + } // public class CallEntry : DependencyObject + + /// + /// Interaction logic for CallHistoryWindow.xaml. + /// public partial class CallHistoryWindow : Window { + /* + ** Properties + */ + + /// + /// + /// public CallHistoryViewModel ViewModel { get; set; } + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// public CallHistoryWindow() { InitializeComponent(); @@ -28,12 +107,22 @@ namespace DVMConsole DataContext = ViewModel; } + /// + /// + /// + /// protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { e.Cancel = true; this.Hide(); } + /// + /// + /// + /// + /// + /// public void AddCall(string channel, int srcId, int dstId) { Dispatcher.Invoke(() => @@ -48,6 +137,12 @@ namespace DVMConsole }); } + /// + /// + /// + /// + /// + /// public void ChannelKeyed(string channel, int srcId, bool encrypted) { Dispatcher.Invoke(() => @@ -63,6 +158,11 @@ namespace DVMConsole }); } + /// + /// + /// + /// + /// public void ChannelUnkeyed(string channel, int srcId) { Dispatcher.Invoke(() => @@ -73,31 +173,5 @@ namespace DVMConsole } }); } - } - - public class CallHistoryViewModel - { - public ObservableCollection CallHistory { get; set; } - - public CallHistoryViewModel() - { - CallHistory = new ObservableCollection(); - } - } - - public class CallEntry : DependencyObject - { - public string Channel { get; set; } - public int SrcId { get; set; } - public int DstId { get; set; } - - public static readonly DependencyProperty BackgroundColorProperty = - DependencyProperty.Register(nameof(BackgroundColor), typeof(Brush), typeof(CallEntry), new PropertyMetadata(Brushes.Transparent)); - - public Brush BackgroundColor - { - get { return (Brush)GetValue(BackgroundColorProperty); } - set { SetValue(BackgroundColorProperty, value); } - } - } -} + } // public partial class CallHistoryWindow : Window +} // namespace dvmconsole diff --git a/DVMConsole/ChannelPosition.cs b/DVMConsole/ChannelPosition.cs deleted file mode 100644 index 3c3ff9e..0000000 --- a/DVMConsole/ChannelPosition.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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 index 41300c3..9fa4371 100644 --- a/DVMConsole/Codeplug.cs +++ b/DVMConsole/Codeplug.cs @@ -1,94 +1,206 @@ // SPDX-License-Identifier: AGPL-3.0-only /** -* Digital Voice Modem - DVMConsole +* Digital Voice Modem - Desktop Dispatch Console * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * -* @package DVM / DVM Console +* @package DVM / Desktop Dispatch Console * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) * * Copyright (C) 2024-2025 Caleb, K4PHP +* Copyright (C) 2025 Bryan Biedenkapp, N2PLL * */ -using System.Security.Policy; +using fnecore.P25; -namespace DVMConsole +namespace dvmconsole { - /// - /// Codeplug object used project wide + /// Codeplug object used to configure the console. /// public class Codeplug { + /* + ** Properties + */ + + /// + /// + /// public List Systems { get; set; } + /// + /// + /// public List Zones { get; set; } + /* + ** Classes + */ + /// /// /// public class System { + /* + ** Properties + */ + + /// + /// + /// public string Name { get; set; } + /// + /// + /// public string Identity { get; set; } + /// + /// + /// public string Address { get; set; } + /// + /// + /// public string Password { get; set; } + /// + /// + /// public string PresharedKey { get; set; } + /// + /// + /// public bool Encrypted { get; set; } + /// + /// + /// public uint PeerId { get; set; } + /// + /// + /// public int Port { get; set; } + /// + /// + /// public string Rid { get; set; } + /// + /// + /// public string AliasPath { get; set; } = "./alias.yml"; + /// + /// + /// public List RidAlias { get; set; } = null; + /* + ** Methods + */ + + /// + /// + /// + /// public override string ToString() { return Name; } - } + } // public class System /// /// /// public class Zone { + /* + ** Properties + */ + + /// + /// + /// public string Name { get; set; } + /// + /// + /// public List Channels { get; set; } - } + } // public class Zone /// /// /// public class Channel { + /* + ** Properties + */ + + /// + /// + /// 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 Algo { get; set; } = "none"; + /// + /// + /// public string KeyId { get; set; } + /* + ** Methods + */ + + /// + /// + /// + /// public ushort GetKeyId() { return Convert.ToUInt16(KeyId, 16); } + /// + /// + /// + /// public byte GetAlgoId() { - return Convert.ToByte(AlgoId, 16); + switch (Algo.ToLowerInvariant()) + { + case "aes": + return P25Defines.P25_ALGO_AES; + case "arc4": + return P25Defines.P25_ALGO_ARC4; + default: + return P25Defines.P25_ALGO_UNENCRYPT; + } } + /// + /// + /// + /// public byte[] GetEncryptionKey() { if (EncryptionKey == null) return []; - return EncryptionKey - .Split(',') - .Select(s => Convert.ToByte(s.Trim(), 16)) - .ToArray(); + return EncryptionKey.Split(',').Select(s => Convert.ToByte(s.Trim(), 16)).ToArray(); } - } + } // public class Channel /// /// Helper to return a system by looking up a @@ -111,10 +223,9 @@ namespace DVMConsole { var channel = zone.Channels.FirstOrDefault(c => c.Name == channelName); if (channel != null) - { return Systems.FirstOrDefault(s => s.Name == channel.System); - } } + return null; } @@ -129,11 +240,10 @@ namespace DVMConsole { var channel = zone.Channels.FirstOrDefault(c => c.Name == channelName); if (channel != null) - { return channel; - } } + return null; } - } -} \ No newline at end of file + } //public class Codeplug +} // namespace dvmconsole diff --git a/DVMConsole/ConsoleNative.cs b/DVMConsole/ConsoleNative.cs deleted file mode 100644 index a91db88..0000000 --- a/DVMConsole/ConsoleNative.cs +++ /dev/null @@ -1,29 +0,0 @@ -// 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/DVMConsole/DVMConsole.csproj b/DVMConsole/DVMConsole.csproj index 5c115e0..83efebd 100644 --- a/DVMConsole/DVMConsole.csproj +++ b/DVMConsole/DVMConsole.csproj @@ -9,6 +9,9 @@ AnyCPU;x64;x86 Debug;Release;WIN32 true + Copyright (c) 2025 Caleb, K4PHP and DVMProject (https://github.com/dvmproject) Authors. + x86 + AGPL-3.0-only @@ -28,7 +31,6 @@ - @@ -94,9 +96,6 @@ Always - - Always - @@ -121,7 +120,7 @@ - + diff --git a/DVMConsole/DigitalPageWindow.xaml b/DVMConsole/DigitalPageWindow.xaml index 5ba7268..5ef98c2 100644 --- a/DVMConsole/DigitalPageWindow.xaml +++ b/DVMConsole/DigitalPageWindow.xaml @@ -1,9 +1,9 @@ - diff --git a/DVMConsole/DigitalPageWindow.xaml.cs b/DVMConsole/DigitalPageWindow.xaml.cs index 4cb9dc5..014b207 100644 --- a/DVMConsole/DigitalPageWindow.xaml.cs +++ b/DVMConsole/DigitalPageWindow.xaml.cs @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only /** -* Digital Voice Modem - DVMConsole +* Digital Voice Modem - Desktop Dispatch Console * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * -* @package DVM / DVM Console +* @package DVM / Desktop Dispatch Console * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) * * Copyright (C) 2025 Caleb, K4PHP @@ -13,10 +13,10 @@ using System.Windows; -namespace DVMConsole +namespace dvmconsole { /// - /// Interaction logic for DigitalPageWindow.xaml + /// Interaction logic for DigitalPageWindow.xaml. /// public partial class DigitalPageWindow : Window { @@ -25,6 +25,10 @@ namespace DVMConsole public string DstId = string.Empty; public Codeplug.System RadioSystem = null; + /// + /// Initializes a new instance of the class. + /// + /// public DigitalPageWindow(List systems) { InitializeComponent(); @@ -35,6 +39,11 @@ namespace DVMConsole SystemCombo.SelectedIndex = 0; } + /// + /// + /// + /// + /// private void SendPageButton_Click(object sender, RoutedEventArgs e) { RadioSystem = SystemCombo.SelectedItem as Codeplug.System; @@ -42,5 +51,5 @@ namespace DVMConsole DialogResult = true; Close(); } - } -} + } // public partial class DigitalPageWindow : Window +} // namespace dvmconsole diff --git a/DVMConsole/FlashingBackgroundManager.cs b/DVMConsole/FlashingBackgroundManager.cs index 6a66542..094fa5b 100644 --- a/DVMConsole/FlashingBackgroundManager.cs +++ b/DVMConsole/FlashingBackgroundManager.cs @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only /** -* Digital Voice Modem - DVMConsole +* Digital Voice Modem - Desktop Dispatch Console * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * -* @package DVM / DVM Console +* @package DVM / Desktop Dispatch Console * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) * * Copyright (C) 2025 Caleb, K4PHP @@ -16,96 +16,125 @@ using System.Windows.Controls; using System.Windows.Media; using System.Windows.Threading; -namespace DVMConsole +namespace dvmconsole { + /// + /// + /// public class FlashingBackgroundManager { - private readonly Control _control; - private readonly Canvas _canvas; - private readonly UserControl _userControl; - private readonly Window _mainWindow; - private readonly DispatcherTimer _timer; - private Brush _originalControlBackground; - private Brush _originalCanvasBackground; - private Brush _originalUserControlBackground; - private Brush _originalMainWindowBackground; - private bool _isFlashing; - + private readonly Control control; + private readonly Canvas canvas; + private readonly UserControl userControl; + private readonly Window mainWindow; + private readonly DispatcherTimer timer; + + private Brush originalControlBackground; + private Brush originalCanvasBackground; + private Brush originalUserControlBackground; + private Brush originalMainWindowBackground; + + private bool isFlashing; + + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// public FlashingBackgroundManager(Control control = null, Canvas canvas = null, UserControl userControl = null, Window mainWindow = null, int intervalMilliseconds = 450) { - _control = control; - _canvas = canvas; - _userControl = userControl; - _mainWindow = mainWindow; + this.control = control; + this.canvas = canvas; + this.userControl = userControl; + this.mainWindow = mainWindow; - if (_control == null && _canvas == null && _userControl == null && _mainWindow == null) + if (this.control == null && this.canvas == null && this.userControl == null && this.mainWindow == null) throw new ArgumentException("At least one of control, canvas, userControl, or mainWindow must be provided."); - _timer = new DispatcherTimer + timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(intervalMilliseconds) }; - _timer.Tick += OnTimerTick; + timer.Tick += OnTimerTick; } + /// + /// + /// public void Start() { - if (_isFlashing) + if (isFlashing) return; - if (_control != null) - _originalControlBackground = _control.Background; + if (control != null) + originalControlBackground = control.Background; - if (_canvas != null) - _originalCanvasBackground = _canvas.Background; + if (canvas != null) + originalCanvasBackground = canvas.Background; - if (_userControl != null) - _originalUserControlBackground = _userControl.Background; + if (userControl != null) + originalUserControlBackground = userControl.Background; - if (_mainWindow != null) - _originalMainWindowBackground = _mainWindow.Background; + if (mainWindow != null) + originalMainWindowBackground = mainWindow.Background; - _isFlashing = true; - _timer.Start(); + isFlashing = true; + timer.Start(); } - + + /// + /// + /// public void Stop() { - if (!_isFlashing) + if (!isFlashing) return; - _timer.Stop(); + timer.Stop(); - if (_control != null) - _control.Background = _originalControlBackground; + if (control != null) + control.Background = originalControlBackground; - if (_canvas != null) - _canvas.Background = _originalCanvasBackground; + if (canvas != null) + canvas.Background = originalCanvasBackground; - if (_userControl != null) - _userControl.Background = _originalUserControlBackground; + if (userControl != null) + userControl.Background = originalUserControlBackground; - if (_mainWindow != null && _originalMainWindowBackground != null) - _mainWindow.Background = _originalMainWindowBackground; + if (mainWindow != null && originalMainWindowBackground != null) + mainWindow.Background = originalMainWindowBackground; - _isFlashing = false; + isFlashing = false; } + /// + /// + /// + /// + /// private void OnTimerTick(object sender, EventArgs e) { Brush flashingColor = Brushes.Red; - if (_control != null) - _control.Background = _control.Background == Brushes.DarkRed ? _originalControlBackground : Brushes.DarkRed; + if (control != null) + control.Background = control.Background == Brushes.DarkRed ? originalControlBackground : Brushes.DarkRed; - if (_canvas != null) - _canvas.Background = _canvas.Background == flashingColor ? _originalCanvasBackground : flashingColor; + if (canvas != null) + canvas.Background = canvas.Background == flashingColor ? originalCanvasBackground : flashingColor; - if (_userControl != null) - _userControl.Background = _userControl.Background == Brushes.DarkRed ? _originalUserControlBackground : Brushes.DarkRed; + if (userControl != null) + userControl.Background = userControl.Background == Brushes.DarkRed ? originalUserControlBackground : Brushes.DarkRed; - if (_mainWindow != null) - _mainWindow.Background = _mainWindow.Background == flashingColor ? _originalMainWindowBackground : flashingColor; + if (mainWindow != null) + mainWindow.Background = mainWindow.Background == flashingColor ? originalMainWindowBackground : flashingColor; } - } -} + } // public class FlashingBackgroundManager +} // namespace dvmconsole diff --git a/DVMConsole/FneSystemBase.DMR.cs b/DVMConsole/FneSystemBase.DMR.cs index 1a85812..a782e91 100644 --- a/DVMConsole/FneSystemBase.DMR.cs +++ b/DVMConsole/FneSystemBase.DMR.cs @@ -11,12 +11,12 @@ * */ -using fnecore.DMR; using fnecore; +using fnecore.DMR; using NAudio.Wave; -namespace DVMConsole +namespace dvmconsole { /// /// Implements a FNE system base. @@ -113,4 +113,4 @@ namespace DVMConsole return; } } // public abstract partial class FneSystemBase : fnecore.FneSystemBase -} \ No newline at end of file +} // namespace dvmconsole diff --git a/DVMConsole/FneSystemBase.NXDN.cs b/DVMConsole/FneSystemBase.NXDN.cs index 99301d9..10b312c 100644 --- a/DVMConsole/FneSystemBase.NXDN.cs +++ b/DVMConsole/FneSystemBase.NXDN.cs @@ -11,10 +11,10 @@ * */ -using fnecore.NXDN; using fnecore; +using fnecore.NXDN; -namespace DVMConsole +namespace dvmconsole { /// /// Implements a FNE system base. @@ -54,4 +54,4 @@ namespace DVMConsole return; } } // public abstract partial class FneSystemBase : fnecore.FneSystemBase -} +} // namespace dvmconsole diff --git a/DVMConsole/FneSystemBase.P25.cs b/DVMConsole/FneSystemBase.P25.cs index 67e4fe8..b4e40c5 100644 --- a/DVMConsole/FneSystemBase.P25.cs +++ b/DVMConsole/FneSystemBase.P25.cs @@ -15,8 +15,31 @@ using fnecore; using fnecore.P25; -namespace DVMConsole +namespace dvmconsole { + /// + /// + /// + public class CryptoParams + { + /* + ** Properties + */ + + /// + /// Message Indicator + /// + public byte[] MI { get; set; } = new byte[P25Defines.P25_MI_LENGTH]; + /// + /// Algorithm ID. + /// + public byte AlgId { get; set; } = P25Defines.P25_ALGO_UNENCRYPT; + /// + /// Key ID. + /// + public ushort KeyId { get; set; } + } // public class CryptoParams + /// /// Implements a FNE system base. /// @@ -55,11 +78,20 @@ namespace DVMConsole return; } + /// + /// + /// + /// + /// + /// + /// + /// + /// public void CreateNewP25MessageHdr(byte duid, RemoteCallData callData, ref byte[] data, byte algId = 0, ushort kId = 0, byte[] mi = null) { CreateP25MessageHdr(duid, callData, ref data); - // if an mi is present, this is an encrypted header + // if an MI is present, this is an encrypted header if (mi != null) { data[14U] |= 0x08; // Control bit @@ -329,25 +361,25 @@ namespace DVMConsole break; case P25DFSI.P25_DFSI_LDU2_VOICE12: { - dfsiFrame[1U] = cryptoParams.Mi[0]; // Message Indicator - dfsiFrame[2U] = cryptoParams.Mi[1]; - dfsiFrame[3U] = cryptoParams.Mi[2]; + 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 } break; case P25DFSI.P25_DFSI_LDU2_VOICE13: { - dfsiFrame[1U] = cryptoParams.Mi[3]; // Message Indicator - dfsiFrame[2U] = cryptoParams.Mi[4]; - dfsiFrame[3U] = cryptoParams.Mi[5]; + 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 } break; case P25DFSI.P25_DFSI_LDU2_VOICE14: { - dfsiFrame[1U] = cryptoParams.Mi[6]; // Message Indicator - dfsiFrame[2U] = cryptoParams.Mi[7]; - dfsiFrame[3U] = cryptoParams.Mi[8]; + 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 } break; @@ -468,14 +500,4 @@ namespace DVMConsole } } } // public abstract partial class FneSystemBase : fnecore.FneSystemBase - - /// - /// - /// - public class CryptoParams - { - public byte[] Mi { get; set; } = new byte[P25Defines.P25_MI_LENGTH]; - public byte AlgId { get; set; } = P25Defines.P25_ALGO_UNENCRYPT; - public ushort KeyId { get; set; } - } -} \ No newline at end of file +} // namespace dvmconsole diff --git a/DVMConsole/FneSystemBase.cs b/DVMConsole/FneSystemBase.cs index f3ea2f0..8955d5e 100644 --- a/DVMConsole/FneSystemBase.cs +++ b/DVMConsole/FneSystemBase.cs @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only /** -* Digital Voice Modem - DVMConsole +* Digital Voice Modem - Desktop Dispatch Console * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * -* @package DVM / DVM Console +* @package DVM / Desktop Dispatch Console * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) * * Copyright (C) 2022-2024 Bryan Biedenkapp, N2PLL @@ -12,11 +12,11 @@ * */ -using fnecore.DMR; using fnecore; +using fnecore.DMR; using fnecore.P25.kmm; -namespace DVMConsole +namespace dvmconsole { /// /// Represents the individual timeslot data status. @@ -196,11 +196,8 @@ namespace DVMConsole byte[] payload = e.Data.Skip(11).ToArray(); //Console.WriteLine(FneUtils.HexDump(payload)); - if (e.MessageId == (byte)KmmMessageType.MODIFY_KEY_CMD) - { mainWindow.KeyResponseReceived(e); - } } /// @@ -213,4 +210,4 @@ namespace DVMConsole } } // public abstract partial class FneSystemBase : fnecore.FneSystemBase -} \ No newline at end of file +} // namespace dvmconsole diff --git a/DVMConsole/FneSystemManager.cs b/DVMConsole/FneSystemManager.cs index 356aa89..cadceaf 100644 --- a/DVMConsole/FneSystemManager.cs +++ b/DVMConsole/FneSystemManager.cs @@ -1,31 +1,35 @@ // SPDX-License-Identifier: AGPL-3.0-only /** -* Digital Voice Modem - DVMConsole +* Digital Voice Modem - Desktop Dispatch Console * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * -* @package DVM / DVM Console +* @package DVM / Desktop Dispatch Console * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) * * Copyright (C) 2025 Caleb, K4PHP * */ -namespace DVMConsole +namespace dvmconsole { /// - /// WhackerLink peer/client websocket manager for having multiple systems + /// /// public class FneSystemManager { - private readonly Dictionary _webSocketHandlers; + private readonly Dictionary peerHandlers; + + /* + ** Methods + */ /// - /// Creates an instance of + /// Creates an instance of class. /// public FneSystemManager() { - _webSocketHandlers = new Dictionary(); + peerHandlers = new Dictionary(); } /// @@ -34,10 +38,8 @@ namespace DVMConsole /// public void AddFneSystem(string systemId, Codeplug.System system, MainWindow mainWindow) { - if (!_webSocketHandlers.ContainsKey(systemId)) - { - _webSocketHandlers[systemId] = new PeerSystem(mainWindow, system); - } + if (!peerHandlers.ContainsKey(systemId)) + peerHandlers[systemId] = new PeerSystem(mainWindow, system); } /// @@ -48,10 +50,9 @@ namespace DVMConsole /// public PeerSystem GetFneSystem(string systemId) { - if (_webSocketHandlers.TryGetValue(systemId, out var handler)) - { + if (peerHandlers.TryGetValue(systemId, out var handler)) return handler; - } + throw new KeyNotFoundException($"WebSocketHandler for system '{systemId}' not found."); } @@ -61,10 +62,10 @@ namespace DVMConsole /// public void RemoveFneSystem(string systemId) { - if (_webSocketHandlers.TryGetValue(systemId, out var handler)) + if (peerHandlers.TryGetValue(systemId, out var handler)) { handler.peer.Stop(); - _webSocketHandlers.Remove(systemId); + peerHandlers.Remove(systemId); } } @@ -75,7 +76,7 @@ namespace DVMConsole /// public bool HasFneSystem(string systemId) { - return _webSocketHandlers.ContainsKey(systemId); + return peerHandlers.ContainsKey(systemId); } /// @@ -83,11 +84,10 @@ namespace DVMConsole /// public void ClearAll() { - foreach (var handler in _webSocketHandlers.Values) - { + foreach (var handler in peerHandlers.Values) handler.peer.Stop(); - } - _webSocketHandlers.Clear(); + + peerHandlers.Clear(); } - } -} + } // public class FneSystemManager +} // namespace dvmconsole diff --git a/DVMConsole/GainSampleProvider.cs b/DVMConsole/GainSampleProvider.cs index 06dfff4..85d5229 100644 --- a/DVMConsole/GainSampleProvider.cs +++ b/DVMConsole/GainSampleProvider.cs @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only /** -* Digital Voice Modem - DVMConsole +* Digital Voice Modem - Desktop Dispatch Console * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * -* @package DVM / DVM Console +* @package DVM / Desktop Dispatch Console * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) * * Copyright (C) 2025 Caleb, K4PHP @@ -12,40 +12,64 @@ */ using NAudio.Wave; -using NAudio.Wave.SampleProviders; -using System; -namespace DVMConsole +namespace dvmconsole { + /// + /// + /// public class GainSampleProvider : ISampleProvider { - private readonly ISampleProvider _source; - private float _gain = 1.0f; + private readonly ISampleProvider source; + private float gain = 1.0f; - public GainSampleProvider(ISampleProvider source) - { - _source = source ?? throw new ArgumentNullException(nameof(source)); - WaveFormat = source.WaveFormat; - } + /* + ** Properties + */ + /// + /// + /// public WaveFormat WaveFormat { get; } + /// + /// + /// public float Gain { - get => _gain; - set => _gain = Math.Max(0, value); + get => gain; + set => gain = Math.Max(0, value); } - public int Read(float[] buffer, int offset, int count) + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public GainSampleProvider(ISampleProvider source) { - int samplesRead = _source.Read(buffer, offset, count); + this.source = source ?? throw new ArgumentNullException(nameof(source)); + WaveFormat = source.WaveFormat; + } + /// + /// + /// + /// + /// + /// + /// + public int Read(float[] buffer, int offset, int count) + { + int samplesRead = source.Read(buffer, offset, count); for (int i = 0; i < samplesRead; i++) - { - buffer[offset + i] *= _gain; - } + buffer[offset + i] *= gain; return samplesRead; } - } -} + } // public class GainSampleProvider : ISampleProvider +} // namespace dvmconsole diff --git a/DVMConsole/KeyStatusWindow.xaml b/DVMConsole/KeyStatusWindow.xaml index fa9f55f..fbc4c32 100644 --- a/DVMConsole/KeyStatusWindow.xaml +++ b/DVMConsole/KeyStatusWindow.xaml @@ -1,4 +1,4 @@ - + /// + /// + public class KeyStatusItem { - public ObservableCollection KeyStatusItems { get; private set; } = new ObservableCollection(); + /* + ** Properties + */ + /// + /// + /// + public string ChannelName { get; set; } + /// + /// + /// + public string AlgId { get; set; } + /// + /// + /// + public string KeyId { get; set; } + /// + /// + /// + public string KeyStatus { get; set; } + } // public class KeyStatusItem + + /// + /// + /// + public partial class KeyStatusWindow : Window + { private Codeplug Codeplug; private MainWindow mainWindow; + /* + ** Properties + */ + + /// + /// + /// + public ObservableCollection KeyStatusItems { get; private set; } = new ObservableCollection(); + + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// + /// + /// public KeyStatusWindow(Codeplug codeplug, MainWindow mainWindow) { InitializeComponent(); @@ -34,6 +82,9 @@ namespace DVMConsole LoadKeyStatus(); } + /// + /// + /// private void LoadKeyStatus() { Dispatcher.Invoke(() => @@ -44,7 +95,7 @@ namespace DVMConsole { if (child == null) { - Console.WriteLine("A child in ChannelsCanvas.Children is null."); + Trace.WriteLine("A child in ChannelsCanvas.Children is null."); continue; } @@ -56,14 +107,14 @@ namespace DVMConsole Codeplug.System system = Codeplug.GetSystemForChannel(channelBox.ChannelName); if (system == null) { - Console.WriteLine($"System not found for {channelBox.ChannelName}"); + Trace.WriteLine($"System not found for {channelBox.ChannelName}"); continue; } Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channelBox.ChannelName); if (cpgChannel == null) { - Console.WriteLine($"Channel not found for {channelBox.ChannelName}"); + Trace.WriteLine($"Channel not found for {channelBox.ChannelName}"); continue; } @@ -72,7 +123,7 @@ namespace DVMConsole if (channelBox.crypter == null) { - Console.WriteLine($"Crypter is null for channel {channelBox.ChannelName}"); + Trace.WriteLine($"Crypter is null for channel {channelBox.ChannelName}"); continue; } @@ -88,13 +139,5 @@ namespace DVMConsole } }); } - } - - public class KeyStatusItem - { - public string ChannelName { get; set; } - public string AlgId { get; set; } - public string KeyId { get; set; } - public string KeyStatus { get; set; } - } -} + } // public partial class KeyStatusWindow : Window +} // namespace dvmconsole diff --git a/DVMConsole/MBEToneDetector.cs b/DVMConsole/MBEToneDetector.cs index 122e90b..9e0b885 100644 --- a/DVMConsole/MBEToneDetector.cs +++ b/DVMConsole/MBEToneDetector.cs @@ -1,15 +1,24 @@ -// From github.com/w3axl/rc2-dvm +// SPDX-License-Identifier: AGPL-3.0-only +/** +* Digital Voice Modem - Desktop Dispatch Console +* AGPLv3 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Desktop Dispatch Console +* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) +* +* Copyright (C) 2025 Patrick McDonnell, W3AXL +* +*/ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using NWaves.Signals; using NWaves.Transforms; -namespace DVMConsole +namespace dvmconsole { + /// + /// + /// public class MBEToneDetector { // Samplerate is 8000 Hz @@ -41,6 +50,10 @@ namespace DVMConsole // The STFT (short-time fourier transform) operator private Stft stft; + /* + ** Methods + */ + /// /// Create a pitch detector which reports the running average of pitch for a sequence of samples /// @@ -123,5 +136,5 @@ namespace DVMConsole } return 0; } - } -} + } // public class MBEToneDetector +} // namespace dvmconsole diff --git a/DVMConsole/MainWindow.xaml b/DVMConsole/MainWindow.xaml index 50ef77a..8868269 100644 --- a/DVMConsole/MainWindow.xaml +++ b/DVMConsole/MainWindow.xaml @@ -1,8 +1,8 @@ - + xmlns:local="clr-namespace:dvmconsole.Controls" + Title="Digital Voice Modem - Desktop Dispatch Console" Height="600" Width="1000" Background="#FFF2F2F2"> diff --git a/DVMConsole/MainWindow.xaml.cs b/DVMConsole/MainWindow.xaml.cs index 7a70298..0c212e4 100644 --- a/DVMConsole/MainWindow.xaml.cs +++ b/DVMConsole/MainWindow.xaml.cs @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only /** -* Digital Voice Modem - DVMConsole +* Digital Voice Modem - Desktop Dispatch Console * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * -* @package DVM / DVM Console +* @package DVM / Desktop Dispatch Console * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) * * Copyright (C) 2024-2025 Caleb, K4PHP @@ -12,46 +12,73 @@ * */ -using Microsoft.Win32; -using System; -using System.Timers; +using System.Diagnostics; using System.IO; +using System.Timers; 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 Microsoft.Win32; + using NAudio.Wave; -using fnecore.P25; -using fnecore; +using NWaves.Signals; + +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +using dvmconsole.Controls; +using static dvmconsole.P25Crypto; + using Constants = fnecore.Constants; +using fnecore; +using fnecore.P25; using fnecore.P25.LC.TSBK; -using NWaves.Signals; -using static DVMConsole.P25Crypto; -namespace DVMConsole + +namespace dvmconsole { + /// + /// + /// + public class ChannelPosition + { + /* + ** Properties + */ + + /// + /// + /// + public double X { get; set; } + /// + /// + /// + public double Y { get; set; } + } // public class ChannelPosition + + /// + /// Interaction logic for MainWindow.xaml. + /// 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 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 SettingsManager settingsManager = new SettingsManager(); + private SelectedChannelsManager selectedChannelsManager; + private FlashingBackgroundManager flashingManager; + private WaveFilePlaybackManager emergencyAlertPlayback; private ChannelBox playbackChannelBox; @@ -61,45 +88,60 @@ namespace DVMConsole public static string PLAYBACKSYS = "LOCPLAYBACKSYS"; public static string PLAYBACKCHNAME = "PLAYBACK"; - private readonly WaveInEvent _waveIn; - private readonly AudioManager _audioManager; + private readonly WaveInEvent waveIn; + private readonly AudioManager audioManager; - private static System.Timers.Timer _channelHoldTimer; + private static System.Timers.Timer channelHoldTimer; private Dictionary systemStatuses = new Dictionary(); - private FneSystemManager _fneSystemManager = new FneSystemManager(); + private FneSystemManager fneSystemManager = new FneSystemManager(); + + /* + ** Properties + */ + + /// + /// + /// + public Codeplug Codeplug { get; set; } + + /* + ** Methods + */ + /// + /// Initializes a new instance of the class. + /// 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")); + 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; + 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 = new WaveInEvent { WaveFormat = new WaveFormat(8000, 16, 1) }; + waveIn.DataAvailable += WaveIn_DataAvailable; + waveIn.RecordingStopped += WaveIn_RecordingStopped; - _waveIn.StartRecording(); + waveIn.StartRecording(); - _audioManager = new AudioManager(_settingsManager); + audioManager = new AudioManager(settingsManager); - _selectedChannelsManager.SelectedChannelsChanged += SelectedChannelsChanged; + selectedChannelsManager.SelectedChannelsChanged += SelectedChannelsChanged; Loaded += MainWindow_Loaded; } + /// + /// + /// + /// + /// private void OpenCodeplug_Click(object sender, RoutedEventArgs e) { OpenFileDialog openFileDialog = new OpenFileDialog @@ -107,21 +149,31 @@ namespace DVMConsole Filter = "Codeplug Files (*.yml)|*.yml|All Files (*.*)|*.*", Title = "Open Codeplug" }; + if (openFileDialog.ShowDialog() == true) { LoadCodeplug(openFileDialog.FileName); - _settingsManager.LastCodeplugPath = openFileDialog.FileName; - _settingsManager.SaveSettings(); + 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 @@ -142,6 +194,9 @@ namespace DVMConsole } } + /// + /// + /// private void GenerateChannelWidgets() { ChannelsCanvas.Children.Clear(); @@ -154,7 +209,7 @@ namespace DVMConsole { var systemStatusBox = new SystemStatusBox(system.Name, system.Address, system.Port); - if (_settingsManager.SystemStatusPositions.TryGetValue(system.Name, out var position)) + if (settingsManager.SystemStatusPositions.TryGetValue(system.Name, out var position)) { Canvas.SetLeft(systemStatusBox, position.X); Canvas.SetTop(systemStatusBox, position.Y); @@ -181,9 +236,9 @@ namespace DVMConsole if (File.Exists(system.AliasPath)) system.RidAlias = AliasTools.LoadAliases(system.AliasPath); - _fneSystemManager.AddFneSystem(system.Name, system, this); + fneSystemManager.AddFneSystem(system.Name, system, this); - PeerSystem peer = _fneSystemManager.GetFneSystem(system.Name); + PeerSystem peer = fneSystemManager.GetFneSystem(system.Name); peer.peer.PeerConnected += (sender, response) => { @@ -213,25 +268,25 @@ namespace DVMConsole peer.Start(); }); - if (!_settingsManager.ShowSystemStatus) + if (!settingsManager.ShowSystemStatus) systemStatusBox.Visibility = Visibility.Collapsed; } } - if (_settingsManager.ShowChannels && Codeplug != null) + 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); + 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)) + if (settingsManager.ChannelPositions.TryGetValue(channel.Name, out var position)) { Canvas.SetLeft(channelBox, position.X); Canvas.SetTop(channelBox, position.Y); @@ -262,18 +317,14 @@ namespace DVMConsole } } - if (_settingsManager.ShowAlertTones && Codeplug != null) + if (settingsManager.ShowAlertTones && Codeplug != null) { - foreach (var alertPath in _settingsManager.AlertToneFilePaths) + foreach (var alertPath in settingsManager.AlertToneFilePaths) { - var alertTone = new AlertTone(alertPath) - { - IsEditMode = isEditMode - }; - + var alertTone = new AlertTone(alertPath) { IsEditMode = isEditMode }; alertTone.OnAlertTone += SendAlertTone; - if (_settingsManager.AlertTonePositions.TryGetValue(alertPath, out var position)) + if (settingsManager.AlertTonePositions.TryGetValue(alertPath, out var position)) { Canvas.SetLeft(alertTone, position.X); Canvas.SetTop(alertTone, position.Y); @@ -290,9 +341,9 @@ namespace DVMConsole } } - playbackChannelBox = new ChannelBox(_selectedChannelsManager, _audioManager, PLAYBACKCHNAME, PLAYBACKSYS, PLAYBACKTG); + playbackChannelBox = new ChannelBox(selectedChannelsManager, audioManager, PLAYBACKCHNAME, PLAYBACKSYS, PLAYBACKTG); - if (_settingsManager.ChannelPositions.TryGetValue(PLAYBACKCHNAME, out var pos)) + if (settingsManager.ChannelPositions.TryGetValue(PLAYBACKCHNAME, out var pos)) { Canvas.SetLeft(playbackChannelBox, pos.X); Canvas.SetTop(playbackChannelBox, pos.Y); @@ -323,16 +374,26 @@ namespace DVMConsole 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()) + foreach (ChannelBox channel in selectedChannelsManager.GetSelectedChannels()) { if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG) { @@ -345,7 +406,7 @@ namespace DVMConsole Task.Run(() => { - PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name); + PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); if (channel.IsSelected && channel.PttState) { @@ -358,25 +419,24 @@ namespace DVMConsole foreach (byte[] chunk in channel.chunkedPcm) { if (chunk.Length == samples) - { P25EncodeAudioFrame(chunk, handler, channel, cpgChannel, system); - } else - { - Console.WriteLine("bad sample length: " + chunk.Length); - } + Trace.WriteLine("bad sample length: " + chunk.Length); } } }); } if (isAnyTgOn && playbackChannelBox.IsSelected) - _audioManager.AddTalkgroupStream(PLAYBACKTG, e.Buffer); + audioManager.AddTalkgroupStream(PLAYBACKTG, e.Buffer); } + /// + /// + /// private void SelectedChannelsChanged() { - foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels()) + foreach (ChannelBox channel in selectedChannelsManager.GetSelectedChannels()) { if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG) continue; @@ -384,7 +444,7 @@ namespace DVMConsole Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); - PeerSystem fne = _fneSystemManager.GetFneSystem(system.Name); + PeerSystem fne = fneSystemManager.GetFneSystem(system.Name); if (channel.IsSelected) { @@ -396,27 +456,37 @@ namespace DVMConsole } } + /// + /// + /// + /// + /// 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 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)); + PeerSystem handler = fneSystemManager.GetFneSystem(pageWindow.RadioSystem.Name); + IOSP_CALL_ALRT callAlert = new IOSP_CALL_ALRT(uint.Parse(pageWindow.DstId), uint.Parse(pageWindow.RadioSystem.Rid)); RemoteCallData callData = new RemoteCallData { - SrcId = UInt32.Parse(pageWindow.RadioSystem.Rid), - DstId = UInt32.Parse(pageWindow.DstId), + SrcId = uint.Parse(pageWindow.RadioSystem.Rid), + DstId = uint.Parse(pageWindow.DstId), LCO = P25Defines.TSBK_IOSP_CALL_ALRT }; @@ -431,17 +501,22 @@ namespace DVMConsole } } + /// + /// + /// + /// + /// 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()) + 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); + PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); if (channel.PageState) { @@ -464,7 +539,7 @@ namespace DVMConsole Task.Run(() => { //_waveProvider.ClearBuffer(); - _audioManager.AddTalkgroupStream(cpgChannel.Tgid, combinedAudio); + audioManager.AddTalkgroupStream(cpgChannel.Tgid, combinedAudio); }); await Task.Run(() => @@ -478,16 +553,14 @@ namespace DVMConsole 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); + handler.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), false); Dispatcher.Invoke(() => { @@ -499,25 +572,34 @@ namespace DVMConsole } } + /// + /// + /// + /// 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()) + 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); + PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); if (channel.PageState || (forHold && channel.HoldState)) { @@ -554,7 +636,7 @@ namespace DVMConsole Task.Run(() => { - _audioManager.AddTalkgroupStream(cpgChannel.Tgid, pcmData); + audioManager.AddTalkgroupStream(cpgChannel.Tgid, pcmData); }); DateTime startTime = DateTime.UtcNow; @@ -565,31 +647,27 @@ namespace DVMConsole byte[] chunk = new byte[chunkSize]; Buffer.BlockCopy(pcmData, offset, chunk, 0, chunkSize); - PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name); + 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); + handler.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), false); Dispatcher.Invoke(() => { @@ -613,26 +691,36 @@ namespace DVMConsole } } + /// + /// + /// + /// + /// 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; + settingsManager.ShowSystemStatus = widgetSelectionWindow.ShowSystemStatus; + settingsManager.ShowChannels = widgetSelectionWindow.ShowChannels; + settingsManager.ShowAlertTones = widgetSelectionWindow.ShowAlertTones; GenerateChannelWidgets(); - _settingsManager.SaveSettings(); + settingsManager.SaveSettings(); } } + /// + /// + /// + /// + /// private void HandleEmergency(string dstId, string srcId) { bool forUs = false; - foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels()) + foreach (ChannelBox channel in selectedChannelsManager.GetSelectedChannels()) { Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); @@ -649,18 +737,28 @@ namespace DVMConsole { Dispatcher.Invoke(() => { - _flashingManager.Start(); - _emergencyAlertPlayback.Start(); + 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) @@ -668,18 +766,19 @@ namespace DVMConsole Codeplug.System system = Codeplug.GetSystemForChannel(e.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(e.ChannelName); - PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name); + PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); if (e.PageState) - { - handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), true); - } + handler.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), true); else - { - handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), false); - } + handler.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), false); } + /// + /// + /// + /// + /// private void ChannelBox_PTTButtonClicked(object sender, ChannelBox e) { if (e.SystemName == PLAYBACKSYS || e.ChannelName == PLAYBACKCHNAME || e.DstId == PLAYBACKTG) @@ -687,89 +786,118 @@ namespace DVMConsole Codeplug.System system = Codeplug.GetSystemForChannel(e.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(e.ChannelName); - PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name); + 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); + uint srcId = uint.Parse(system.Rid); + uint dstId = uint.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; + 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; + 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; + 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)); + 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); + 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); - } + 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; + if (!isEditMode || !isDragging || draggedElement == null) + return; - _isDragging = false; - _draggedElement.ReleaseMouseCapture(); - _draggedElement = null; + 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 (!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 }; + settingsManager.SystemStatusPositions[systemStatusBox.SystemName] = new ChannelPosition { X = x, Y = y }; ChannelBox_MouseRightButtonDown(sender, e); @@ -777,6 +905,11 @@ namespace DVMConsole } } + /// + /// + /// + /// + /// private void ToggleEditMode_Click(object sender, RoutedEventArgs e) { isEditMode = !isEditMode; @@ -785,22 +918,26 @@ namespace DVMConsole 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 @@ -812,14 +949,11 @@ namespace DVMConsole if (openFileDialog.ShowDialog() == true) { string alertFilePath = openFileDialog.FileName; - var alertTone = new AlertTone(alertFilePath) - { - IsEditMode = isEditMode - }; + var alertTone = new AlertTone(alertFilePath) { IsEditMode = isEditMode }; alertTone.OnAlertTone += SendAlertTone; - if (_settingsManager.AlertTonePositions.TryGetValue(alertFilePath, out var position)) + if (settingsManager.AlertTonePositions.TryGetValue(alertFilePath, out var position)) { Canvas.SetLeft(alertTone, position.X); Canvas.SetTop(alertTone, position.Y); @@ -833,12 +967,17 @@ namespace DVMConsole alertTone.MouseRightButtonUp += AlertTone_MouseRightButtonUp; ChannelsCanvas.Children.Add(alertTone); - _settingsManager.UpdateAlertTonePaths(alertFilePath); + settingsManager.UpdateAlertTonePaths(alertFilePath); AdjustCanvasHeight(); } } + /// + /// + /// + /// + /// private void AlertTone_MouseRightButtonUp(object sender, MouseButtonEventArgs e) { if (!isEditMode) return; @@ -847,12 +986,15 @@ namespace DVMConsole { double x = Canvas.GetLeft(alertTone); double y = Canvas.GetTop(alertTone); - _settingsManager.UpdateAlertTonePosition(alertTone.AlertFilePath, x, y); + settingsManager.UpdateAlertTonePosition(alertTone.AlertFilePath, x, y); AdjustCanvasHeight(); } } + /// + /// + /// private void AdjustCanvasHeight() { double maxBottom = 0; @@ -861,40 +1003,44 @@ namespace DVMConsole { 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); - } + 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()) + 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); + 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); + handler.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), true); await Task.Delay(1000); SendAlertTone("hold.wav", true); @@ -902,24 +1048,36 @@ namespace DVMConsole } } + /// + /// + /// + /// protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { - _settingsManager.SaveSettings(); + settingsManager.SaveSettings(); base.OnClosing(e); Application.Current.Shutdown(); } + /// + /// + /// + /// + /// private void ClearEmergency_Click(object sender, RoutedEventArgs e) { - _emergencyAlertPlayback.Stop(); - _flashingManager.Stop(); + emergencyAlertPlayback.Stop(); + flashingManager.Stop(); - foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels()) - { + foreach (ChannelBox channel in selectedChannelsManager.GetSelectedChannels()) channel.Emergency = false; - } } + /// + /// + /// + /// + /// private void btnAlert1_Click(object sender, RoutedEventArgs e) { Dispatcher.Invoke(() => { @@ -927,6 +1085,11 @@ namespace DVMConsole }); } + /// + /// + /// + /// + /// private void btnAlert2_Click(object sender, RoutedEventArgs e) { Dispatcher.Invoke(() => @@ -935,6 +1098,11 @@ namespace DVMConsole }); } + /// + /// + /// + /// + /// private void btnAlert3_Click(object sender, RoutedEventArgs e) { Dispatcher.Invoke(() => @@ -943,6 +1111,11 @@ namespace DVMConsole }); } + /// + /// + /// + /// + /// private async void btnGlobalPtt_Click(object sender, RoutedEventArgs e) { if (globalPttState) @@ -950,14 +1123,14 @@ namespace DVMConsole globalPttState = !globalPttState; - foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels()) + 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); + PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); channel.txStreamId = handler.NewStreamId(); @@ -969,7 +1142,7 @@ namespace DVMConsole channel.PttState = true; }); - handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), true); + handler.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), true); } else { @@ -979,11 +1152,16 @@ namespace DVMConsole channel.PttState = false; }); - handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), false); + handler.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), false); } } } + /// + /// + /// + /// + /// private void SelectAll_Click(object sender, RoutedEventArgs e) { foreach (ChannelBox channel in ChannelsCanvas.Children.OfType()) @@ -1001,13 +1179,9 @@ namespace DVMConsole channel.Background = channel.IsSelected ? (Brush)new BrushConverter().ConvertFrom("#FF0B004B") : Brushes.Gray; if (channel.IsSelected) - { - _selectedChannelsManager.AddSelectedChannel(channel); - } + selectedChannelsManager.AddSelectedChannel(channel); else - { - _selectedChannelsManager.RemoveSelectedChannel(channel); - } + selectedChannelsManager.RemoveSelectedChannel(channel); } } } @@ -1065,6 +1239,7 @@ namespace DVMConsole { tone = channel.toneDetector.Detect(signal); } + if (tone > 0) { MBEToneGenerator.IMBEEncodeSingleTone((ushort)tone, imbe); @@ -1101,17 +1276,17 @@ namespace DVMConsole } } - channel.crypter.Prepare(cpgChannel.GetAlgoId(), cpgChannel.GetKeyId(), ProtocolType.P25Phase1, channel.mi); + channel.crypter.Prepare(cpgChannel.GetAlgoId(), cpgChannel.GetKeyId(), channel.mi); } // crypto time - channel.crypter.Process(imbe, channel.p25N < 9U ? P25Crypto.FrameType.LDU1 : P25Crypto.FrameType.LDU2, 0); + channel.crypter.Process(imbe, channel.p25N < 9U ? P25DUID.LDU1 : P25DUID.LDU2); // 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); + channel.crypter.Prepare(cpgChannel.GetAlgoId(), cpgChannel.GetKeyId(), channel.mi); } } @@ -1177,8 +1352,8 @@ namespace DVMConsole break; } - uint srcId = UInt32.Parse(system.Rid); - uint dstId = UInt32.Parse(cpgChannel.Tgid); + uint srcId = uint.Parse(system.Rid); + uint dstId = uint.Parse(cpgChannel.Tgid); FnePeer peer = handler.peer; RemoteCallData callData = new RemoteCallData() @@ -1219,7 +1394,7 @@ namespace DVMConsole 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 }); + 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); } @@ -1233,7 +1408,7 @@ namespace DVMConsole /// /// /// - private void P25DecodeAudioFrame(byte[] ldu, P25DataReceivedEvent e, PeerSystem system, ChannelBox channel, bool emergency = false, P25Crypto.FrameType frameType = P25Crypto.FrameType.LDU1) + private void P25DecodeAudioFrame(byte[] ldu, P25DataReceivedEvent e, PeerSystem system, ChannelBox channel, bool emergency = false, P25DUID duid = P25DUID.LDU1) { try { @@ -1276,7 +1451,7 @@ namespace DVMConsole short[] samples = new short[FneSystemBase.MBE_SAMPLES_LENGTH]; - channel.crypter.Process(imbe, frameType, n); + channel.crypter.Process(imbe, duid); #if WIN32 if (channel.extFullRateVocoder == null) @@ -1311,7 +1486,7 @@ namespace DVMConsole pcmIdx += 2; } - _audioManager.AddTalkgroupStream(e.DstId.ToString(), pcmData); + audioManager.AddTalkgroupStream(e.DstId.ToString(), pcmData); } } } @@ -1345,11 +1520,11 @@ namespace DVMConsole Dispatcher.Invoke(() => { - foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels()) + 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); + PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); if (cpgChannel.GetKeyId() != 0 && cpgChannel.GetAlgoId() != 0) channel.crypter.AddKey(key.KeyId, e.KmmKey.KeysetItem.AlgId, key.GetKey()); @@ -1358,6 +1533,11 @@ namespace DVMConsole } } + /// + /// + /// + /// + /// private void KeyStatus_Click(object sender, RoutedEventArgs e) { KeyStatusWindow keyStatus = new KeyStatusWindow(Codeplug, this); @@ -1382,7 +1562,7 @@ namespace DVMConsole Dispatcher.Invoke(() => { - foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels()) + foreach (ChannelBox channel in selectedChannelsManager.GetSelectedChannels()) { Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); @@ -1390,7 +1570,7 @@ namespace DVMConsole bool isEmergency = false; bool encrypted = false; - PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name); + PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); if (!channel.IsEnabled) continue; @@ -1399,14 +1579,10 @@ namespace DVMConsole 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]; @@ -1422,7 +1598,7 @@ namespace DVMConsole 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); + channel.crypter.Prepare(channel.algId, channel.kId, channel.mi); encrypted = true; } @@ -1593,14 +1769,14 @@ namespace DVMConsole 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); + P25DecodeAudioFrame(channel.netLDU2, e, handler, channel, isEmergency, P25DUID.LDU2); } } break; } if (channel.mi != null) - channel.crypter.Prepare(channel.algId, channel.kId, P25Crypto.ProtocolType.P25Phase1, channel.mi); + channel.crypter.Prepare(channel.algId, channel.kId, channel.mi); slot.RxRFS = e.SrcId; slot.RxType = e.FrameType; @@ -1612,9 +1788,14 @@ namespace DVMConsole }); } + /// + /// + /// + /// + /// private void CallHist_Click(object sender, RoutedEventArgs e) { callHistoryWindow.Show(); } - } -} + } // public partial class MainWindow : Window +} // namespace dvmconsole diff --git a/DVMConsole/P25Crypto.cs b/DVMConsole/P25Crypto.cs index 7cf41ff..c1b7b1f 100644 --- a/DVMConsole/P25Crypto.cs +++ b/DVMConsole/P25Crypto.cs @@ -1,51 +1,104 @@ // SPDX-License-Identifier: AGPL-3.0-only /** -* Digital Voice Modem - DVMConsole +* Digital Voice Modem - Desktop Dispatch Console * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * -* @package DVM / DVM Console +* @package DVM / Desktop Dispatch Console * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) * * Copyright (C) 2025 Caleb, K4PHP * */ -// TODO: Move to fnecore - -using System; -using System.Collections.Generic; using System.Security.Cryptography; -using System.Linq; -namespace DVMConsole +using fnecore.P25; + +namespace dvmconsole { + /// + /// + /// public class P25Crypto { - private ProtocolType protocol; + public const int IMBE_BUF_LEN = 11; + private byte algId; private ushort keyId; + private byte[] messageIndicator = new byte[9]; + private Dictionary keys = new Dictionary(); + private byte[] aesKeystream = new byte[240]; // AES buffer private byte[] adpKeystream = new byte[469]; // ADP buffer - private int aesPosition; - private int adpPosition; + private int ksPosition; + + /* + ** Class + */ + + /// + /// + /// + private class KeyInfo + { + /* + ** Properties + */ + + /// + /// + /// + public byte AlgId { get; } + /// + /// + /// + public byte[] Key { get; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public KeyInfo(byte algid, byte[] key) + { + AlgId = algid; + Key = key; + } + } // private class KeyInfo + + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// public P25Crypto() { - this.protocol = ProtocolType.Unknown; - this.algId = 0x80; + this.algId = P25Defines.P25_ALGO_UNENCRYPT; this.keyId = 0; - this.aesPosition = 0; - this.adpPosition = 0; + + this.ksPosition = 0; } + /// + /// + /// public void Reset() { keys.Clear(); } + /// + /// + /// + /// + /// + /// public void AddKey(ushort keyid, byte algid, byte[] key) { if (keyid == 0 || algid == 0x80) @@ -54,58 +107,90 @@ namespace DVMConsole keys[keyid] = new KeyInfo(algid, key); } + /// + /// + /// + /// + /// public bool HasKey(ushort keyId) { return keys.ContainsKey(keyId); } - public bool Prepare(byte algid, ushort keyid, ProtocolType protocol, byte[] MI) + /// + /// + /// + /// + /// + /// + /// + /// + public bool Prepare(byte algid, ushort keyid, byte[] MI) { this.algId = algid; this.keyId = keyid; - this.protocol = protocol; + Array.Copy(MI, this.messageIndicator, Math.Min(MI.Length, this.messageIndicator.Length)); if (!keys.ContainsKey(keyid)) return false; - if (algid == 0x84) // AES-256 + this.ksPosition = 0; + + if (algid == P25Defines.P25_ALGO_AES) { - this.aesPosition = 0; - GenerateAesKeystream(); + GenerateAESKeystream(); return true; } - else if (algid == 0xAA) // ADP (RC4) + else if (algid == P25Defines.P25_ALGO_ARC4) { - this.adpPosition = 0; - GenerateAdpKeystream(); + GenerateARC4Keystream(); return true; } return false; } - public bool Process(byte[] PCW, FrameType frameType, int voiceSubframe) + /// + /// + /// + /// + /// + /// + public bool Process(byte[] imbe, P25DUID duid) { if (!keys.ContainsKey(keyId)) return false; return algId switch { - 0x84 => AesProcess(PCW, frameType, voiceSubframe), - 0xAA => AdpProcess(PCW, frameType, voiceSubframe), + P25Defines.P25_ALGO_AES => AESProcess(imbe, duid), + P25Defines.P25_ALGO_ARC4 => ARC4Process(imbe, duid), _ => false }; } /// - /// Create ADP key stream + /// + /// + /// + /// + /// + private void Swap(byte[] a, int i1, int i2) + { + byte temp = a[i1]; + a[i1] = a[i2]; + a[i2] = temp; + } + + /// + /// Create ARC4 keystream. /// - private void GenerateAdpKeystream() + private void GenerateARC4Keystream() { byte[] adpKey = new byte[13]; - byte[] S = new byte[256]; - byte[] K = new byte[256]; + byte[] permutation = new byte[256]; + byte[] key = new byte[256]; if (!keys.ContainsKey(keyId)) return; @@ -127,141 +212,46 @@ namespace DVMConsole adpKey[i] = messageIndicator[i - 5]; } + // generate ARC4 keystream + // initialize state variable for (i = 0; i < 256; ++i) { - K[i] = adpKey[i % 13]; - S[i] = (byte)i; + key[i] = adpKey[i % 13]; + permutation[i] = (byte)i; } + // randomize, using key for (i = 0; i < 256; ++i) { - j = (j + S[i] + K[i]) & 0xFF; - Swap(S, i, j); + j = (j + permutation[i] + key[i]) & 0xFF; + Swap(permutation, i, j); } + // perform RC4 transformation i = j = 0; for (k = 0; k < 469; ++k) { i = (i + 1) & 0xFF; - j = (j + S[i]) & 0xFF; - Swap(S, i, j); - adpKeystream[k] = S[(S[i] + S[j]) & 0xFF]; - } - } - - /// - /// Preform a swap - /// - /// - /// - /// - private void Swap(byte[] S, int i, int j) - { - byte temp = S[i]; - S[i] = S[j]; - S[j] = temp; - } - - /// - /// Process AES256 - /// - /// - /// - /// - /// - private bool AesProcess(byte[] PCW, FrameType frameType, int voiceSubframe) - { - int offset = 16; - - switch (frameType) - { - case FrameType.LDU1: offset += 0; break; - case FrameType.LDU2: offset += 101; break; - case FrameType.V4_0: offset += 7 * voiceSubframe; break; - case FrameType.V4_1: offset += 7 * (voiceSubframe + 4); break; - case FrameType.V4_2: offset += 7 * (voiceSubframe + 8); break; - case FrameType.V4_3: offset += 7 * (voiceSubframe + 12); break; - case FrameType.V2: offset += 7 * (voiceSubframe + 16); break; - default: return false; - } - - if (protocol == ProtocolType.P25Phase1) - { - offset += (aesPosition * 11) + 11 + (aesPosition < 8 ? 0 : 2); - aesPosition = (aesPosition + 1) % 9; - - for (int j = 0; j < 11; ++j) - { - PCW[j] ^= aesKeystream[j + offset]; - } - } - else if (protocol == ProtocolType.P25Phase2) - { - for (int j = 0; j < 7; ++j) - { - PCW[j] ^= aesKeystream[j + offset]; - } - PCW[6] &= 0x80; - } - - return true; - } - - /// - /// Process ADP - /// - /// - /// - /// - /// - private bool AdpProcess(byte[] PCW, FrameType frameType, int voiceSubframe) - { - int offset = 256; + j = (j + permutation[i]) & 0xFF; - switch (frameType) - { - case FrameType.LDU1: offset = 0; break; - case FrameType.LDU2: offset = 101; break; - case FrameType.V4_0: offset += 7 * voiceSubframe; break; - case FrameType.V4_1: offset += 7 * (voiceSubframe + 4); break; - case FrameType.V4_2: offset += 7 * (voiceSubframe + 8); break; - case FrameType.V4_3: offset += 7 * (voiceSubframe + 12); break; - case FrameType.V2: offset += 7 * (voiceSubframe + 16); break; - default: return false; - } + // swap permutation[i] and permutation[j] + Swap(permutation, i, j); - if (protocol == ProtocolType.P25Phase1) - { - offset += (adpPosition * 11) + 267 + (adpPosition < 8 ? 0 : 2); - adpPosition = (adpPosition + 1) % 9; - - for (int j = 0; j < 11; ++j) - { - PCW[j] ^= adpKeystream[j + offset]; - } + // transform byte + adpKeystream[k] = permutation[(permutation[i] + permutation[j]) & 0xFF]; } - else if (protocol == ProtocolType.P25Phase2) - { - for (int j = 0; j < 7; ++j) - { - PCW[j] ^= adpKeystream[j + offset]; - } - PCW[6] &= 0x80; - } - - return true; } /// - /// Create AES key stream + /// Create AES keystream. /// - private void GenerateAesKeystream() + private void GenerateAESKeystream() { if (!keys.ContainsKey(keyId)) return; byte[] key = keys[keyId].Key; - byte[] iv = ExpandMiTo128(messageIndicator); + byte[] iv = ExpandMIToIV(messageIndicator); using (var aes = Aes.Create()) { @@ -287,6 +277,48 @@ namespace DVMConsole } } + /// + /// Helper to process IMBE audio using AES-256. + /// + /// + /// + /// + private bool AESProcess(byte[] imbe, P25DUID duid) + { + int offset = 16; + if (duid == P25DUID.LDU2) + offset += 101; + + offset += (ksPosition * IMBE_BUF_LEN) + IMBE_BUF_LEN + (ksPosition < 8 ? 0 : 2); + ksPosition = (ksPosition + 1) % 9; + + for (int j = 0; j < IMBE_BUF_LEN; ++j) + imbe[j] ^= aesKeystream[j + offset]; + + return true; + } + + /// + /// Helper to process IMBE audio using ARC4. + /// + /// + /// + /// + private bool ARC4Process(byte[] imbe, P25DUID duid) + { + int offset = 256; + if (duid != P25DUID.LDU2) + offset += 101; + + offset += (ksPosition * IMBE_BUF_LEN) + 267 + (ksPosition < 8 ? 0 : 2); + ksPosition = (ksPosition + 1) % 9; + + for (int j = 0; j < IMBE_BUF_LEN; ++j) + imbe[j] ^= adpKeystream[j + offset]; + + return true; + } + /// /// Cycle P25 LFSR /// @@ -350,7 +382,7 @@ namespace DVMConsole /// /// /// - private static byte[] ExpandMiTo128(byte[] mi) + private static byte[] ExpandMIToIV(byte[] mi) { if (mi == null || mi.Length < 8) throw new ArgumentException("MI must be at least 8 bytes long."); @@ -360,16 +392,12 @@ namespace DVMConsole // Copy first 64 bits of MI into LFSR ulong lfsr = 0; for (int i = 0; i < 8; i++) - { lfsr = (lfsr << 8) | mi[i]; - } // Use LFSR routine to compute the expansion ulong overflow = 0; for (int i = 0; i < 64; i++) - { overflow = (overflow << 1) | StepP25Lfsr(ref lfsr); - } // Copy expansion and LFSR to IV for (int i = 7; i >= 0; i--) @@ -377,6 +405,7 @@ namespace DVMConsole iv[i] = (byte)(overflow & 0xFF); overflow >>= 8; } + for (int i = 15; i >= 8; i--) { iv[i] = (byte)(lfsr & 0xFF); @@ -385,37 +414,5 @@ namespace DVMConsole return iv; } - - - private class KeyInfo - { - public byte AlgId { get; } - public byte[] Key { get; } - - public KeyInfo(byte algid, byte[] key) - { - AlgId = algid; - Key = key; - } - } - - public enum ProtocolType - { - Unknown = 0, - P25Phase1, - P25Phase2 - } - - public enum FrameType - { - Unknown = 0, - LDU1, - LDU2, - V2, - V4_0, - V4_1, - V4_2, - V4_3 - } - } -} + } // public class P25Crypto +} // namespace dvmconsole diff --git a/DVMConsole/PeerSystem.cs b/DVMConsole/PeerSystem.cs index dfe722f..6b2c8be 100644 --- a/DVMConsole/PeerSystem.cs +++ b/DVMConsole/PeerSystem.cs @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only /** -* Digital Voice Modem - DVMConsole +* Digital Voice Modem - Desktop Dispatch Console * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * -* @package DVM / DVM Console +* @package DVM / Desktop Dispatch Console * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) * * Copyright (C) 2023 Bryan Biedenkapp, N2PLL @@ -16,7 +16,7 @@ using System.Net; using fnecore; -namespace DVMConsole +namespace dvmconsole { /// /// Implements a peer FNE router system. @@ -113,4 +113,4 @@ namespace DVMConsole /* stub */ } } // public class PeerSystem -} // namespace rc2_dvm \ No newline at end of file +} // namespace dvmconsole diff --git a/DVMConsole/QuickCallPage.xaml b/DVMConsole/QuickCallPage.xaml index e563b43..2c7a4a1 100644 --- a/DVMConsole/QuickCallPage.xaml +++ b/DVMConsole/QuickCallPage.xaml @@ -1,9 +1,9 @@ - diff --git a/DVMConsole/QuickCallPage.xaml.cs b/DVMConsole/QuickCallPage.xaml.cs index ff2b286..322b70f 100644 --- a/DVMConsole/QuickCallPage.xaml.cs +++ b/DVMConsole/QuickCallPage.xaml.cs @@ -1,26 +1,19 @@ -/* -* WhackerLink - DVMConsole +// SPDX-License-Identifier: AGPL-3.0-only +/** +* Digital Voice Modem - Desktop Dispatch Console +* 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 / Desktop Dispatch 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 System.Windows; -namespace DVMConsole +namespace dvmconsole { /// /// Interaction logic for QuickCallPage.xaml @@ -30,11 +23,23 @@ namespace DVMConsole public string ToneA; public string ToneB; + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// public QuickCallPage() { InitializeComponent(); } + /// + /// + /// + /// + /// private void SendButton_Click(object sender, RoutedEventArgs e) { ToneA = ToneAText.Text; @@ -43,5 +48,5 @@ namespace DVMConsole DialogResult = true; Close(); } - } -} + } // public partial class QuickCallPage : Window +} // namespace dvmconsole diff --git a/DVMConsole/SampleTimeConvert.cs b/DVMConsole/SampleTimeConvert.cs index 65109d1..4fe08e6 100644 --- a/DVMConsole/SampleTimeConvert.cs +++ b/DVMConsole/SampleTimeConvert.cs @@ -13,7 +13,7 @@ using NAudio.Wave; -namespace DVMConsole +namespace dvmconsole { /// /// @@ -67,5 +67,5 @@ namespace DVMConsole { return ToBytes(format, ToSamples(format, ms)); } - } // public class SamplesToMS -} // namespace dvmbridge \ No newline at end of file + } // public class SampleTimeConvert +} // namespace dvmconsole diff --git a/DVMConsole/SelectedChannelsManager.cs b/DVMConsole/SelectedChannelsManager.cs index 11203ee..4ffb0c2 100644 --- a/DVMConsole/SelectedChannelsManager.cs +++ b/DVMConsole/SelectedChannelsManager.cs @@ -1,59 +1,86 @@ // SPDX-License-Identifier: AGPL-3.0-only /** -* Digital Voice Modem - DVMConsole +* Digital Voice Modem - Desktop Dispatch Console * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * -* @package DVM / DVM Console +* @package DVM / Desktop Dispatch Console * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) * * Copyright (C) 2024 Caleb, K4PHP * */ -using DVMConsole.Controls; +using dvmconsole.Controls; -namespace DVMConsole +namespace dvmconsole { + /// + /// + /// public class SelectedChannelsManager { - private readonly HashSet _selectedChannels; + private readonly HashSet selectedChannels; + public IReadOnlyCollection GetSelectedChannels() => selectedChannels; + + /* + ** Events + */ + + /// + /// + /// public event Action SelectedChannelsChanged; + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// public SelectedChannelsManager() { - _selectedChannels = new HashSet(); + selectedChannels = new HashSet(); } + /// + /// + /// + /// public void AddSelectedChannel(ChannelBox channel) { - if (_selectedChannels.Add(channel)) + if (selectedChannels.Add(channel)) { channel.IsSelected = true; SelectedChannelsChanged.Invoke(); } } + /// + /// + /// + /// public void RemoveSelectedChannel(ChannelBox channel) { - if (_selectedChannels.Remove(channel)) + if (selectedChannels.Remove(channel)) { channel.IsSelected = false; SelectedChannelsChanged.Invoke(); } } + /// + /// + /// public void ClearSelections() { - foreach (var channel in _selectedChannels) - { + foreach (var channel in selectedChannels) channel.IsSelected = false; - } - _selectedChannels.Clear(); + + selectedChannels.Clear(); SelectedChannelsChanged.Invoke(); } - - public IReadOnlyCollection GetSelectedChannels() => _selectedChannels; - } -} + } // public class SelectedChannelsManager +} // namespace dvmconsole diff --git a/DVMConsole/SettingsManager.cs b/DVMConsole/SettingsManager.cs index a7e83d6..4de5863 100644 --- a/DVMConsole/SettingsManager.cs +++ b/DVMConsole/SettingsManager.cs @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only /** -* Digital Voice Modem - DVMConsole +* Digital Voice Modem - Desktop Dispatch Console * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * -* @package DVM / DVM Console +* @package DVM / Desktop Dispatch Console * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) * * Copyright (C) 2024-2025 Caleb, K4PHP @@ -12,26 +12,68 @@ */ using System.IO; + using Newtonsoft.Json; -namespace DVMConsole +namespace dvmconsole { + /// + /// + /// public class SettingsManager { private const string SettingsFilePath = "UserSettings.json"; + /* + ** Properties + */ + + /// + /// + /// 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(); + /* + ** Methods + */ + + /// + /// + /// public void LoadSettings() { if (!File.Exists(SettingsFilePath)) return; @@ -60,6 +102,10 @@ namespace DVMConsole } } + /// + /// + /// + /// public void UpdateAlertTonePaths(string newFilePath) { if (!AlertToneFilePaths.Contains(newFilePath)) @@ -69,30 +115,56 @@ namespace DVMConsole } } + /// + /// + /// + /// + /// + /// 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 @@ -105,5 +177,5 @@ namespace DVMConsole Console.WriteLine($"Error saving settings: {ex.Message}"); } } - } -} + } // public class SettingsManager +} // namespace dvmconsole diff --git a/DVMConsole/ToneGenerator.cs b/DVMConsole/ToneGenerator.cs index 8b49fbc..6068fc7 100644 --- a/DVMConsole/ToneGenerator.cs +++ b/DVMConsole/ToneGenerator.cs @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only /** -* Digital Voice Modem - DVMConsole +* Digital Voice Modem - Desktop Dispatch Console * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * -* @package DVM / DVM Console +* @package DVM / Desktop Dispatch Console * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) * * Copyright (C) 2024-2025 Caleb, K4PHP @@ -13,27 +13,31 @@ using NAudio.Wave; -namespace DVMConsole +namespace dvmconsole { /// /// /// public class ToneGenerator { - private readonly int _sampleRate = 8000; - private readonly int _bitsPerSample = 16; - private readonly int _channels = 1; - private WaveOutEvent _waveOut; - private BufferedWaveProvider _waveProvider; + private readonly int sampleRate = 8000; + private readonly int bitsPerSample = 16; + private readonly int channels = 1; + private WaveOutEvent waveOut; + private BufferedWaveProvider waveProvider; + + /* + ** Methods + */ /// - /// Creates an instance of + /// Initializes a new instance of the class. /// public ToneGenerator() { - _waveOut = new WaveOutEvent(); - _waveProvider = new BufferedWaveProvider(new WaveFormat(_sampleRate, _bitsPerSample, _channels)); - _waveOut.Init(_waveProvider); + waveOut = new WaveOutEvent(); + waveProvider = new BufferedWaveProvider(new WaveFormat(sampleRate, bitsPerSample, channels)); + waveOut.Init(waveProvider); } /// @@ -44,12 +48,12 @@ namespace DVMConsole /// PCM data as a byte array public byte[] GenerateTone(double frequency, double durationSeconds) { - int sampleCount = (int)(_sampleRate * durationSeconds); - byte[] buffer = new byte[sampleCount * (_bitsPerSample / 8)]; + int sampleCount = (int)(sampleRate * durationSeconds); + byte[] buffer = new byte[sampleCount * (bitsPerSample / 8)]; for (int i = 0; i < sampleCount; i++) { - double time = (double)i / _sampleRate; + double time = (double)i / sampleRate; short sampleValue = (short)(Math.Sin(2 * Math.PI * frequency * time) * short.MaxValue); buffer[i * 2] = (byte)(sampleValue & 0xFF); @@ -68,10 +72,10 @@ namespace DVMConsole { byte[] toneData = GenerateTone(frequency, durationSeconds); - _waveProvider.ClearBuffer(); - _waveProvider.AddSamples(toneData, 0, toneData.Length); + waveProvider.ClearBuffer(); + waveProvider.AddSamples(toneData, 0, toneData.Length); - _waveOut.Play(); + waveOut.Play(); } /// @@ -79,7 +83,7 @@ namespace DVMConsole /// public void StopTone() { - _waveOut.Stop(); + waveOut.Stop(); } /// @@ -87,7 +91,7 @@ namespace DVMConsole /// public void Dispose() { - _waveOut.Dispose(); + waveOut.Dispose(); } - } -} + } // public class ToneGenerator +} // namespace dvmconsole diff --git a/DVMConsole/VocoderInterop.cs b/DVMConsole/VocoderInterop.cs index 57bd0f9..4b6181a 100644 --- a/DVMConsole/VocoderInterop.cs +++ b/DVMConsole/VocoderInterop.cs @@ -1,28 +1,58 @@ -// From https://github.com/w3axl/rc2-dvm +// SPDX-License-Identifier: AGPL-3.0-only +/** +* Digital Voice Modem - Desktop Dispatch Console +* AGPLv3 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Desktop Dispatch Console +* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) +* +* Copyright (C) 2025 Patrick McDonnell, W3AXL +* +*/ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; -using fnecore; -namespace DVMConsole +namespace dvmconsole { + /// + /// + /// public enum MBE_MODE { DMR_AMBE, //! DMR AMBE IMBE_88BIT, //! 88-bit IMBE (P25) - } + } // public enum MBE_MODE /// - /// Wrapper class for the c++ dvmvocoder encoder library + /// Wrapper class for the C++ dvmvocoder encoder library. /// /// Using info from https://stackoverflow.com/a/315064/1842613 public class MBEEncoder { + private IntPtr encoder; + + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// + /// Vocoder Mode (DMR or P25) + public MBEEncoder(MBE_MODE mode) + { + encoder = MBEEncoder_Create(mode); + } + + /// + /// Finalizes a instance of the class. + /// + ~MBEEncoder() + { + MBEEncoder_Delete(encoder); + } + /// /// Create a new MBEEncoder /// @@ -54,28 +84,6 @@ namespace DVMConsole [DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)] public static extern void MBEEncoder_Delete(IntPtr pEncoder); - /// - /// Pointer to the encoder instance - /// - private IntPtr encoder { get; set; } - - /// - /// Create a new MBEEncoder instance - /// - /// Vocoder Mode (DMR or P25) - public MBEEncoder(MBE_MODE mode) - { - encoder = MBEEncoder_Create(mode); - } - - /// - /// Private class destructor properly deletes interop instance - /// - ~MBEEncoder() - { - MBEEncoder_Delete(encoder); - } - /// /// Encode PCM16 samples to MBE codeword /// @@ -86,17 +94,45 @@ namespace DVMConsole MBEEncoder_Encode(encoder, samples, codeword); } + /// + /// + /// + /// + /// public void encodeBits([In] char[] bits, [Out] byte[] codeword) { MBEEncoder_EncodeBits(encoder, bits, codeword); } - } + } // public class MBEEncoder /// - /// Wrapper class for the c++ dvmvocoder decoder library + /// Wrapper class for the C++ dvmvocoder decoder library. /// public class MBEDecoder { + private IntPtr decoder; + + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// + /// Vocoder Mode (DMR or P25) + public MBEDecoder(MBE_MODE mode) + { + decoder = MBEDecoder_Create(mode); + } + + /// + /// Finalizes a instance of the class. + /// + ~MBEDecoder() + { + MBEDecoder_Delete(decoder); + } + /// /// Create a new MBEDecoder /// @@ -130,28 +166,6 @@ namespace DVMConsole [DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)] public static extern void MBEDecoder_Delete(IntPtr pDecoder); - /// - /// Pointer to the decoder instance - /// - private IntPtr decoder { get; set; } - - /// - /// Create a new MBEDecoder instance - /// - /// Vocoder Mode (DMR or P25) - public MBEDecoder(MBE_MODE mode) - { - decoder = MBEDecoder_Create(mode); - } - - /// - /// Private class destructor properly deletes interop instance - /// - ~MBEDecoder() - { - MBEDecoder_Delete(decoder); - } - /// /// Decode MBE codeword to PCM16 samples /// @@ -172,8 +186,11 @@ namespace DVMConsole { return MBEDecoder_DecodeBits(decoder, codeword, bits); } - } + } // public class MBEDecoder + /// + /// + /// public static class MBEToneGenerator { /// @@ -183,7 +200,7 @@ namespace DVMConsole /// /// /// - public static void AmbeEncodeSingleTone(int tone_freq_hz, char tone_amplitude, [Out] byte[] codeword) + public static void AMBEEncodeSingleTone(int tone_freq_hz, char tone_amplitude, [Out] byte[] codeword) { // U bit vectors // u0 and u1 are 12 bits @@ -197,15 +214,11 @@ namespace DVMConsole // Validate tone index if (tone_idx < 5 || tone_idx > 122) - { throw new ArgumentOutOfRangeException($"Tone index for frequency out of range!"); - } // Validate amplitude value if (tone_amplitude > 127) - { throw new ArgumentOutOfRangeException("Tone amplitude must be between 0 and 127!"); - } // Make sure tone index only has 7 bits (it should but we make sure :) ) tone_idx &= 0b01111111; @@ -258,8 +271,11 @@ namespace DVMConsole byte[] tone_codeword = VocoderToneLookupTable.IMBEToneFrames[nearest]; Array.Copy(tone_codeword, codeword, tone_codeword.Length); } - } + } // public static class MBEToneGenerator + /// + /// + /// public class MBEInterleaver { public const int PCM_SAMPLES = 160; @@ -273,6 +289,14 @@ namespace DVMConsole private MBEEncoder encoder; private MBEDecoder decoder; + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// + /// public MBEInterleaver(MBE_MODE mode) { this.mode = mode; @@ -280,13 +304,19 @@ namespace DVMConsole decoder = new MBEDecoder(this.mode); } - public Int32 Decode([In] byte[] codeword, [Out] byte[] mbeBits) + /// + /// + /// + /// + /// + /// + /// + /// + public int Decode([In] byte[] codeword, [Out] byte[] mbeBits) { // Input validation if (codeword == null) - { throw new NullReferenceException("Input MBE codeword is null!"); - } char[] bits = null; @@ -294,24 +324,18 @@ namespace DVMConsole if (mode == MBE_MODE.DMR_AMBE) { if (codeword.Length != AMBE_CODEWORD_SAMPLES) - { throw new ArgumentOutOfRangeException($"AMBE codeword length is != {AMBE_CODEWORD_SAMPLES}"); - } bits = new char[AMBE_CODEWORD_BITS]; } else if (mode == MBE_MODE.IMBE_88BIT) { if (codeword.Length != IMBE_CODEWORD_SAMPLES) - { throw new ArgumentOutOfRangeException($"IMBE codeword length is != {IMBE_CODEWORD_SAMPLES}"); - } bits = new char[IMBE_CODEWORD_BITS]; } if (bits == null) - { throw new NullReferenceException("Failed to initialize decoder"); - } // Decode int errs = decoder.decodeBits(codeword, bits); @@ -334,12 +358,18 @@ namespace DVMConsole return errs; } + /// + /// + /// + /// + /// + /// + /// + /// public void Encode([In] byte[] mbeBits, [Out] byte[] codeword) { if (mbeBits == null) - { throw new NullReferenceException("Input MBE bit array is null!"); - } char[] bits = null; @@ -347,34 +377,30 @@ namespace DVMConsole if (mode == MBE_MODE.DMR_AMBE) { if (mbeBits.Length != AMBE_CODEWORD_BITS) - { throw new ArgumentOutOfRangeException($"AMBE codeword bit length is != {AMBE_CODEWORD_BITS}"); - } bits = new char[AMBE_CODEWORD_BITS]; Array.Copy(mbeBits, bits, AMBE_CODEWORD_BITS); } else if (mode == MBE_MODE.IMBE_88BIT) { if (mbeBits.Length != IMBE_CODEWORD_BITS) - { throw new ArgumentOutOfRangeException($"IMBE codeword bit length is != {AMBE_CODEWORD_BITS}"); - } bits = new char[IMBE_CODEWORD_BITS]; Array.Copy(mbeBits, bits, IMBE_CODEWORD_BITS); } if (bits == null) - { throw new ArgumentException("Bit array did not get set up properly!"); - } // Encode samples if (mode == MBE_MODE.DMR_AMBE) { // Create output array byte[] codewords = new byte[AMBE_CODEWORD_SAMPLES]; + // Encode encoder.encodeBits(bits, codewords); + // Copy codeword = new byte[AMBE_CODEWORD_SAMPLES]; Array.Copy(codewords, codeword, IMBE_CODEWORD_SAMPLES); @@ -383,12 +409,14 @@ namespace DVMConsole { // Create output array byte[] codewords = new byte[IMBE_CODEWORD_SAMPLES]; + // Encode encoder.encodeBits(bits, codewords); + // Copy codeword = new byte[IMBE_CODEWORD_SAMPLES]; Array.Copy(codewords, codeword, IMBE_CODEWORD_SAMPLES); } } - } -} \ No newline at end of file + } // public class MBEInterleaver +} // namespace dvmconsole diff --git a/DVMConsole/VocoderToneLookupTable.cs b/DVMConsole/VocoderToneLookupTable.cs index a1afcb3..bdcc0f0 100644 --- a/DVMConsole/VocoderToneLookupTable.cs +++ b/DVMConsole/VocoderToneLookupTable.cs @@ -1,4 +1,17 @@ -namespace DVMConsole +// SPDX-License-Identifier: AGPL-3.0-only +/** +* Digital Voice Modem - Desktop Dispatch Console +* AGPLv3 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Desktop Dispatch Console +* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) +* +* Copyright (C) 2025 Patrick McDonnell, W3AXL +* +*/ + +namespace dvmconsole { /// /// From https://github.com/W3AXL/rc2-dvm/blob/main/rc2-dvm/Audio.cs @@ -84,5 +97,5 @@ { 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 } }, }; - } -} + } // public class VocoderToneLookupTable +} // namespace dvmconsole diff --git a/DVMConsole/WaveFilePlaybackManager.cs b/DVMConsole/WaveFilePlaybackManager.cs index ff11299..acdbe39 100644 --- a/DVMConsole/WaveFilePlaybackManager.cs +++ b/DVMConsole/WaveFilePlaybackManager.cs @@ -1,91 +1,125 @@ // SPDX-License-Identifier: AGPL-3.0-only /** -* Digital Voice Modem - DVMConsole +* Digital Voice Modem - Desktop Dispatch Console * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * -* @package DVM / DVM Console +* @package DVM / Desktop Dispatch Console * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) * * Copyright (C) 2024 Caleb, K4PHP * */ -using NAudio.Wave; using System.Windows.Threading; -namespace DVMConsole +using NAudio.Wave; + +namespace dvmconsole { + /// + /// + /// public class WaveFilePlaybackManager { - private readonly string _waveFilePath; - private readonly DispatcherTimer _timer; - private WaveOutEvent _waveOut; - private AudioFileReader _audioFileReader; - private bool _isPlaying; + private readonly string waveFilePath; + private readonly DispatcherTimer timer; + private WaveOutEvent waveOut; + private AudioFileReader audioFileReader; + private bool isPlaying; + + /* + ** Methods + */ + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// public WaveFilePlaybackManager(string waveFilePath, int intervalMilliseconds = 500) { if (string.IsNullOrEmpty(waveFilePath)) throw new ArgumentNullException(nameof(waveFilePath), "Wave file path cannot be null or empty."); - _waveFilePath = waveFilePath; - _timer = new DispatcherTimer + this.waveFilePath = waveFilePath; + timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(intervalMilliseconds) }; - _timer.Tick += OnTimerTick; + timer.Tick += OnTimerTick; } + /// + /// + /// public void Start() { - if (_isPlaying) + if (isPlaying) return; InitializeAudio(); - _isPlaying = true; - _timer.Start(); + isPlaying = true; + timer.Start(); } + /// + /// + /// public void Stop() { - if (!_isPlaying) + if (!isPlaying) return; - _timer.Stop(); + timer.Stop(); DisposeAudio(); - _isPlaying = false; + isPlaying = false; } + /// + /// + /// + /// + /// private void OnTimerTick(object sender, EventArgs e) { PlayAudio(); } + /// + /// + /// private void InitializeAudio() { - _audioFileReader = new AudioFileReader(_waveFilePath); - _waveOut = new WaveOutEvent(); - _waveOut.Init(_audioFileReader); + audioFileReader = new AudioFileReader(waveFilePath); + waveOut = new WaveOutEvent(); + waveOut.Init(audioFileReader); } + /// + /// + /// private void PlayAudio() { - if (_waveOut != null && _waveOut.PlaybackState != PlaybackState.Playing) + if (waveOut != null && waveOut.PlaybackState != PlaybackState.Playing) { - _waveOut.Stop(); - _audioFileReader.Position = 0; - _waveOut.Play(); + waveOut.Stop(); + audioFileReader.Position = 0; + waveOut.Play(); } } + /// + /// + /// private void DisposeAudio() { - _waveOut?.Stop(); - _waveOut?.Dispose(); - _audioFileReader?.Dispose(); - _waveOut = null; - _audioFileReader = null; + waveOut?.Stop(); + waveOut?.Dispose(); + audioFileReader?.Dispose(); + waveOut = null; + audioFileReader = null; } - } -} + } // public class WaveFilePlaybackManager +} // namespace dvmconsole diff --git a/DVMConsole/WidgetSelectionWindow.xaml b/DVMConsole/WidgetSelectionWindow.xaml index 1186f7f..14174b3 100644 --- a/DVMConsole/WidgetSelectionWindow.xaml +++ b/DVMConsole/WidgetSelectionWindow.xaml @@ -1,4 +1,4 @@ - diff --git a/DVMConsole/WidgetSelectionWindow.xaml.cs b/DVMConsole/WidgetSelectionWindow.xaml.cs index 4b160f0..e09f6f4 100644 --- a/DVMConsole/WidgetSelectionWindow.xaml.cs +++ b/DVMConsole/WidgetSelectionWindow.xaml.cs @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only /** -* Digital Voice Modem - DVMConsole +* Digital Voice Modem - Desktop Dispatch Console * AGPLv3 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * -* @package DVM / DVM Console +* @package DVM / Desktop Dispatch Console * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) * * Copyright (C) 2024 Caleb, K4PHP @@ -13,19 +13,47 @@ using System.Windows; -namespace DVMConsole +namespace dvmconsole { + /// + /// Interaction logic for WidgetSelectionWindow.xaml + /// public partial class WidgetSelectionWindow : Window { + /* + ** Properties + */ + + /// + /// + /// public bool ShowSystemStatus { get; private set; } = true; + /// + /// + /// public bool ShowChannels { get; private set; } = true; + /// + /// + /// public bool ShowAlertTones { get; private set; } = true; + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// public WidgetSelectionWindow() { InitializeComponent(); } + /// + /// + /// + /// + /// private void ApplyButton_Click(object sender, RoutedEventArgs e) { ShowSystemStatus = SystemStatusCheckBox.IsChecked ?? false; @@ -34,5 +62,5 @@ namespace DVMConsole DialogResult = true; Close(); } - } -} + } // public partial class WidgetSelectionWindow : Window +} // namespace dvmconsole diff --git a/DVMConsole/codeplugs/codeplug.yml b/DVMConsole/codeplugs/codeplug.yml index 86e3f98..66e4583 100644 --- a/DVMConsole/codeplugs/codeplug.yml +++ b/DVMConsole/codeplugs/codeplug.yml @@ -33,8 +33,8 @@ zones: tgid: "2001" # Encryption Key Id (If 0 or blank, will be assumed clear) keyId: 0x50 - # Algorithm Id 0xAA or 0x84 (RC4 or AES) (If 0 or blank, will be assumed clear) - algoId: 0xaa + # Algorithm AES ("aes"), ADP/ARC4 ("arc4"), None ("none") + algo: "aes" # Ignored now, we use dvmfne KMM support (This will be used in the future to ovveride FNE KMM support) encryptionKey: null - name: "Channel 2" diff --git a/README.md b/README.md index ef1dcb1..84891ba 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,18 @@ -# DVMConsole -### DVM Desktop Console -![console](./images/consolehome.JPG) +# Digital Voice Modem Desktop Dispatch Console + +This provides a desktop application that mimics or otherwise operates like a system dispatch console. + ## Setup - Download the packaged release from the releases or clone and build yourself - Modify the codeplug file - Select the codeplug once opening the app + ## Features - Custumizable widgets - Individual channel audio control - AES and RC4 crypto support -- Auto saved and transferable user settings \ No newline at end of file +- Auto saved and transferable user settings + +## License + +This project is licensed under the AGPLv3 License - see the [LICENSE](LICENSE) file for details. Use of this project is intended, for amateur and/or educational use ONLY. Any other use is at the risk of user and all commercial purposes is strictly discouraged. diff --git a/DVMConsole/AlertTone.xaml b/dvmconsole/Controls/AlertTone.xaml similarity index 92% rename from DVMConsole/AlertTone.xaml rename to dvmconsole/Controls/AlertTone.xaml index a53cd09..3b683cb 100644 --- a/DVMConsole/AlertTone.xaml +++ b/dvmconsole/Controls/AlertTone.xaml @@ -1,4 +1,4 @@ - - - + - - diff --git a/DVMConsole/AlertTone.xaml.cs b/dvmconsole/Controls/AlertTone.xaml.cs similarity index 53% rename from DVMConsole/AlertTone.xaml.cs rename to dvmconsole/Controls/AlertTone.xaml.cs index c3d2101..9dd8071 100644 --- a/DVMConsole/AlertTone.xaml.cs +++ b/dvmconsole/Controls/AlertTone.xaml.cs @@ -1,51 +1,71 @@ -/* -* WhackerLink - DVMConsole +// SPDX-License-Identifier: AGPL-3.0-only +/** +* Digital Voice Modem - Desktop Dispatch Console +* 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 / Desktop Dispatch 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.Media; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; -namespace DVMConsole.Controls +namespace dvmconsole.Controls { + /// + /// + /// public partial class AlertTone : UserControl { - public event Action OnAlertTone; + private Point startPoint; + private bool isDragging; public static readonly DependencyProperty AlertFileNameProperty = DependencyProperty.Register("AlertFileName", typeof(string), typeof(AlertTone), new PropertyMetadata(string.Empty)); + /* + ** Properties + */ + + /// + /// + /// public string AlertFileName { get => (string)GetValue(AlertFileNameProperty); set => SetValue(AlertFileNameProperty, value); } + /// + /// + /// public string AlertFilePath { get; set; } - private Point _startPoint; - private bool _isDragging; - + /// + /// + /// public bool IsEditMode { get; set; } + /* + ** Events + */ + + public event Action OnAlertTone; + + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// + /// public AlertTone(string alertFilePath) { InitializeComponent(); @@ -57,29 +77,44 @@ namespace DVMConsole.Controls this.MouseRightButtonDown += AlertTone_MouseRightButtonDown; } + /// + /// + /// + /// + /// private void PlayAlert_Click(object sender, RoutedEventArgs e) { OnAlertTone.Invoke(this); } + /// + /// + /// + /// + /// private void AlertTone_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (!IsEditMode) return; - _startPoint = e.GetPosition(this); - _isDragging = true; + startPoint = e.GetPosition(this); + isDragging = true; } + /// + /// + /// + /// + /// private void AlertTone_MouseMove(object sender, MouseEventArgs e) { - if (_isDragging && IsEditMode) + if (isDragging && IsEditMode) { var parentCanvas = VisualTreeHelper.GetParent(this) as Canvas; if (parentCanvas != null) { Point mousePos = e.GetPosition(parentCanvas); - double newLeft = mousePos.X - _startPoint.X; - double newTop = mousePos.Y - _startPoint.Y; + double newLeft = mousePos.X - startPoint.X; + double newTop = mousePos.Y - startPoint.Y; Canvas.SetLeft(this, Math.Max(0, newLeft)); Canvas.SetTop(this, Math.Max(0, newTop)); @@ -87,11 +122,16 @@ namespace DVMConsole.Controls } } + /// + /// + /// + /// + /// private void AlertTone_MouseRightButtonDown(object sender, MouseButtonEventArgs e) { - if (!IsEditMode || !_isDragging) return; + if (!IsEditMode || !isDragging) return; - _isDragging = false; + isDragging = false; var parentCanvas = VisualTreeHelper.GetParent(this) as Canvas; if (parentCanvas != null) @@ -102,10 +142,5 @@ namespace DVMConsole.Controls ReleaseMouseCapture(); } - - private void TextBox_TextChanged(object sender, TextChangedEventArgs e) - { - - } - } -} + } // public partial class AlertTone : UserControl +} // namespace dvmconsole.Controls diff --git a/DVMConsole/ChannelBox.xaml b/dvmconsole/Controls/ChannelBox.xaml similarity index 95% rename from DVMConsole/ChannelBox.xaml rename to dvmconsole/Controls/ChannelBox.xaml index 64cb60a..1be0e67 100644 --- a/DVMConsole/ChannelBox.xaml +++ b/dvmconsole/Controls/ChannelBox.xaml @@ -1,4 +1,4 @@ - @@ -42,9 +42,9 @@ + Height="21" VerticalAlignment="Top" x:Name="VolumeSlider" + ValueChanged="VolumeSlider_ValueChanged" Margin="11,10,65,0" + Grid.ColumnSpan="2" Grid.Row="2">