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 683592a..0000000 Binary files a/DVMConsole/Assets/WhackerLinkLogoV4.png and /dev/null differ 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">