project cleanup; file reorg; rewrite README.md;

pull/1/head
Bryan Biedenkapp 1 year ago
parent b9e61d58fb
commit f1a57b593b

@ -3,14 +3,14 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.8.34330.188 VisualStudioVersion = 17.8.34330.188
MinimumVisualStudioVersion = 10.0.40219.1 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 EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B5A7CF60-CCDE-4B2B-85C1-86AE3A19FB31}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B5A7CF60-CCDE-4B2B-85C1-86AE3A19FB31}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig .editorconfig = .editorconfig
EndProjectSection EndProjectSection
EndProject 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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2025 Caleb, K4PHP * Copyright (C) 2025 Caleb, K4PHP
@ -12,23 +12,46 @@
*/ */
using System.IO; using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using YamlDotNet.Serialization.NamingConventions;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
using System.Diagnostics; using YamlDotNet.Serialization.NamingConventions;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary>
///
/// </summary>
public class RadioAlias
{
/// <summary>
///
/// </summary>
public string Alias { get; set; }
/// <summary>
///
/// </summary>
public int Rid { get; set; }
} //public class RadioAlias
/// <summary>
///
/// </summary>
public static class AliasTools public static class AliasTools
{ {
/*
** Methods
*/
/// <summary>
///
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
/// <exception cref="FileNotFoundException"></exception>
public static List<RadioAlias> LoadAliases(string filePath) public static List<RadioAlias> LoadAliases(string filePath)
{ {
if (!File.Exists(filePath)) if (!File.Exists(filePath))
{
throw new FileNotFoundException("Alias file not found.", filePath); throw new FileNotFoundException("Alias file not found.", filePath);
}
var deserializer = new DeserializerBuilder() var deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance) .WithNamingConvention(CamelCaseNamingConvention.Instance)
@ -38,6 +61,12 @@ namespace DVMConsole
return deserializer.Deserialize<List<RadioAlias>>(yamlText); return deserializer.Deserialize<List<RadioAlias>>(yamlText);
} }
/// <summary>
///
/// </summary>
/// <param name="aliases"></param>
/// <param name="rid"></param>
/// <returns></returns>
public static string GetAliasByRid(List<RadioAlias> aliases, int rid) public static string GetAliasByRid(List<RadioAlias> aliases, int rid)
{ {
if (aliases == null || aliases.Count == 0) if (aliases == null || aliases.Count == 0)
@ -46,11 +75,5 @@ namespace DVMConsole
var match = aliases.FirstOrDefault(a => a.Rid == rid); var match = aliases.FirstOrDefault(a => a.Rid == rid);
return match?.Alias ?? string.Empty; return match?.Alias ?? string.Empty;
} }
} } //public static class AliasTools
} // namespace DVMConsole
public class RadioAlias
{
public string Alias { get; set; }
public int Rid { get; set; }
}
}

@ -11,8 +11,10 @@
* *
*/ */
using System.Runtime.InteropServices;
#if WIN32 #if WIN32
namespace DVMConsole namespace dvmconsole
{ {
/// <summary> /// <summary>
/// Implements P/Invoke to callback into external AMBE encoder/decoder library. /// 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); 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 } // public class AmbeVocoder
} }
#endif #endif

@ -1,4 +1,4 @@
<Application x:Class="WhackerLinkConsoleV2.App" <Application x:Class="dvmconsole.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml"> StartupUri="MainWindow.xaml">

@ -1,14 +1,25 @@
using System.Configuration; // SPDX-License-Identifier: AGPL-3.0-only
using System.Data; /**
* 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; using System.Windows;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary> /// <summary>
/// Interaction logic for App.xaml ///
/// </summary> /// </summary>
public partial class App : Application public partial class App : Application
{ {
/* stub */
} }
} // namespace dvmconsole
}

@ -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; using System.Windows;
[assembly: ThemeInfo( [assembly: ThemeInfo(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

@ -1,17 +1,17 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2025 Caleb, K4PHP * Copyright (C) 2025 Caleb, K4PHP
* *
*/ */
namespace DVMConsole namespace dvmconsole
{ {
/// <summary> /// <summary>
/// Helper to convert audio between different chunk sizes /// Helper to convert audio between different chunk sizes

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2025 Caleb, K4PHP * Copyright (C) 2025 Caleb, K4PHP
@ -14,23 +14,27 @@
using NAudio.Wave; using NAudio.Wave;
using NAudio.Wave.SampleProviders; using NAudio.Wave.SampleProviders;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary> /// <summary>
/// Class for managing audio streams /// Class for managing audio streams.
/// </summary> /// </summary>
public class AudioManager public class AudioManager
{ {
private Dictionary<string, (WaveOutEvent waveOut, MixingSampleProvider mixer, BufferedWaveProvider buffer, GainSampleProvider gainProvider)> _talkgroupProviders; private Dictionary<string, (WaveOutEvent waveOut, MixingSampleProvider mixer, BufferedWaveProvider buffer, GainSampleProvider gainProvider)> talkgroupProviders;
private SettingsManager _settingsManager; private SettingsManager settingsManager;
/*
** Methods
*/
/// <summary> /// <summary>
/// Creates an instance of <see cref="AudioManager"/> /// Creates an instance of <see cref="AudioManager"/> class.
/// </summary> /// </summary>
public AudioManager(SettingsManager settingsManager) public AudioManager(SettingsManager settingsManager)
{ {
_settingsManager = settingsManager; this.settingsManager = settingsManager;
_talkgroupProviders = new Dictionary<string, (WaveOutEvent, MixingSampleProvider, BufferedWaveProvider, GainSampleProvider)>(); talkgroupProviders = new Dictionary<string, (WaveOutEvent, MixingSampleProvider, BufferedWaveProvider, GainSampleProvider)>();
} }
/// <summary> /// <summary>
@ -40,10 +44,10 @@ namespace DVMConsole
/// <param name="audioData"></param> /// <param name="audioData"></param>
public void AddTalkgroupStream(string talkgroupId, byte[] audioData) public void AddTalkgroupStream(string talkgroupId, byte[] audioData)
{ {
if (!_talkgroupProviders.ContainsKey(talkgroupId)) if (!talkgroupProviders.ContainsKey(talkgroupId))
AddTalkgroupStream(talkgroupId); AddTalkgroupStream(talkgroupId);
_talkgroupProviders[talkgroupId].buffer.AddSamples(audioData, 0, audioData.Length); talkgroupProviders[talkgroupId].buffer.AddSamples(audioData, 0, audioData.Length);
} }
/// <summary> /// <summary>
@ -52,34 +56,19 @@ namespace DVMConsole
/// <param name="talkgroupId"></param> /// <param name="talkgroupId"></param>
private void AddTalkgroupStream(string talkgroupId) private void AddTalkgroupStream(string talkgroupId)
{ {
int deviceIndex = _settingsManager.ChannelOutputDevices.ContainsKey(talkgroupId) ? _settingsManager.ChannelOutputDevices[talkgroupId] : 0; 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
};
var gainProvider = new GainSampleProvider(bufferProvider.ToSampleProvider()) var waveOut = new WaveOutEvent { DeviceNumber = deviceIndex };
{ var bufferProvider = new BufferedWaveProvider(new WaveFormat(8000, 16, 1)) { DiscardOnBufferOverflow = true };
Gain = 1.0f var gainProvider = new GainSampleProvider(bufferProvider.ToSampleProvider()) { Gain = 1.0f };
}; var mixer = new MixingSampleProvider(WaveFormat.CreateIeeeFloatWaveFormat(8000, 1)) { ReadFully = true };
var mixer = new MixingSampleProvider(WaveFormat.CreateIeeeFloatWaveFormat(8000, 1))
{
ReadFully = true
};
mixer.AddMixerInput(gainProvider); mixer.AddMixerInput(gainProvider);
waveOut.Init(mixer); waveOut.Init(mixer);
waveOut.Play(); waveOut.Play();
_talkgroupProviders[talkgroupId] = (waveOut, mixer, bufferProvider, gainProvider); talkgroupProviders[talkgroupId] = (waveOut, mixer, bufferProvider, gainProvider);
} }
/// <summary> /// <summary>
@ -87,14 +76,12 @@ namespace DVMConsole
/// </summary> /// </summary>
public void SetTalkgroupVolume(string talkgroupId, float volume) public void SetTalkgroupVolume(string talkgroupId, float volume)
{ {
if (_talkgroupProviders.ContainsKey(talkgroupId)) if (talkgroupProviders.ContainsKey(talkgroupId))
{ talkgroupProviders[talkgroupId].gainProvider.Gain = volume;
_talkgroupProviders[talkgroupId].gainProvider.Gain = volume;
}
else else
{ {
AddTalkgroupStream(talkgroupId); AddTalkgroupStream(talkgroupId);
_talkgroupProviders[talkgroupId].gainProvider.Gain = volume; talkgroupProviders[talkgroupId].gainProvider.Gain = volume;
} }
} }
@ -105,13 +92,13 @@ namespace DVMConsole
/// <param name="deviceIndex"></param> /// <param name="deviceIndex"></param>
public void SetTalkgroupOutputDevice(string talkgroupId, int deviceIndex) public void SetTalkgroupOutputDevice(string talkgroupId, int deviceIndex)
{ {
if (_talkgroupProviders.ContainsKey(talkgroupId)) if (talkgroupProviders.ContainsKey(talkgroupId))
{ {
_talkgroupProviders[talkgroupId].waveOut.Stop(); talkgroupProviders[talkgroupId].waveOut.Stop();
_talkgroupProviders.Remove(talkgroupId); talkgroupProviders.Remove(talkgroupId);
} }
_settingsManager.UpdateChannelOutputDevice(talkgroupId, deviceIndex); settingsManager.UpdateChannelOutputDevice(talkgroupId, deviceIndex);
AddTalkgroupStream(talkgroupId); AddTalkgroupStream(talkgroupId);
} }
@ -120,8 +107,8 @@ namespace DVMConsole
/// </summary> /// </summary>
public void Stop() public void Stop()
{ {
foreach (var provider in _talkgroupProviders.Values) foreach (var provider in talkgroupProviders.Values)
provider.waveOut.Stop(); provider.waveOut.Stop();
} }
} } // public class AudioManager
} } // namespace dvmconsole

@ -1,4 +1,4 @@
<Window x:Class="DVMConsole.AudioSettingsWindow" <Window x:Class="dvmconsole.AudioSettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Audio Settings" Height="400" Width="450" Title="Audio Settings" Height="400" Width="450"

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2025 Caleb, K4PHP * Copyright (C) 2025 Caleb, K4PHP
@ -12,47 +12,64 @@
*/ */
using System.Windows; using System.Windows;
using System.Collections.Generic;
using System.Linq;
using NAudio.Wave;
using System.Windows.Controls; using System.Windows.Controls;
namespace DVMConsole using NAudio.Wave;
namespace dvmconsole
{ {
/// <summary>
/// Interaction logic for AudioSettingsWindow.xaml.
/// </summary>
public partial class AudioSettingsWindow : Window public partial class AudioSettingsWindow : Window
{ {
private readonly SettingsManager _settingsManager; private readonly SettingsManager settingsManager;
private readonly AudioManager _audioManager; private readonly AudioManager audioManager;
private readonly List<Codeplug.Channel> _channels; private readonly List<Codeplug.Channel> channels;
private readonly Dictionary<string, int> _selectedOutputDevices = new Dictionary<string, int>(); private readonly Dictionary<string, int> selectedOutputDevices = new Dictionary<string, int>();
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="AudioSettingsWindow"/> class.
/// </summary>
/// <param name="settingsManager"></param>
/// <param name="audioManager"></param>
/// <param name="channels"></param>
public AudioSettingsWindow(SettingsManager settingsManager, AudioManager audioManager, List<Codeplug.Channel> channels) public AudioSettingsWindow(SettingsManager settingsManager, AudioManager audioManager, List<Codeplug.Channel> channels)
{ {
InitializeComponent(); InitializeComponent();
_settingsManager = settingsManager; this.settingsManager = settingsManager;
_audioManager = audioManager; this.audioManager = audioManager;
_channels = channels; this.channels = channels;
LoadAudioDevices(); LoadAudioDevices();
LoadChannelOutputSettings(); LoadChannelOutputSettings();
} }
/// <summary>
///
/// </summary>
private void LoadAudioDevices() private void LoadAudioDevices()
{ {
List<string> inputDevices = GetAudioInputDevices(); List<string> inputDevices = GetAudioInputDevices();
List<string> outputDevices = GetAudioOutputDevices(); List<string> outputDevices = GetAudioOutputDevices();
InputDeviceComboBox.ItemsSource = inputDevices; InputDeviceComboBox.ItemsSource = inputDevices;
InputDeviceComboBox.SelectedIndex = _settingsManager.ChannelOutputDevices.ContainsKey("GLOBAL_INPUT") InputDeviceComboBox.SelectedIndex = settingsManager.ChannelOutputDevices.ContainsKey("GLOBAL_INPUT")
? _settingsManager.ChannelOutputDevices["GLOBAL_INPUT"] ? settingsManager.ChannelOutputDevices["GLOBAL_INPUT"] : 0;
: 0;
} }
/// <summary>
///
/// </summary>
private void LoadChannelOutputSettings() private void LoadChannelOutputSettings()
{ {
List<string> outputDevices = GetAudioOutputDevices(); List<string> outputDevices = GetAudioOutputDevices();
foreach (var channel in _channels) foreach (var channel in channels)
{ {
TextBlock channelLabel = new TextBlock TextBlock channelLabel = new TextBlock
{ {
@ -65,15 +82,15 @@ namespace DVMConsole
{ {
Width = 350, Width = 350,
ItemsSource = outputDevices, ItemsSource = outputDevices,
SelectedIndex = _settingsManager.ChannelOutputDevices.ContainsKey(channel.Tgid) SelectedIndex = settingsManager.ChannelOutputDevices.ContainsKey(channel.Tgid)
? _settingsManager.ChannelOutputDevices[channel.Tgid] ? settingsManager.ChannelOutputDevices[channel.Tgid]
: 0 : 0
}; };
outputDeviceComboBox.SelectionChanged += (s, e) => outputDeviceComboBox.SelectionChanged += (s, e) =>
{ {
int selectedIndex = outputDeviceComboBox.SelectedIndex; int selectedIndex = outputDeviceComboBox.SelectedIndex;
_selectedOutputDevices[channel.Tgid] = selectedIndex; selectedOutputDevices[channel.Tgid] = selectedIndex;
}; };
ChannelOutputStackPanel.Children.Add(channelLabel); ChannelOutputStackPanel.Children.Add(channelLabel);
@ -81,6 +98,10 @@ namespace DVMConsole
} }
} }
/// <summary>
///
/// </summary>
/// <returns></returns>
private List<string> GetAudioInputDevices() private List<string> GetAudioInputDevices()
{ {
List<string> inputDevices = new List<string>(); List<string> inputDevices = new List<string>();
@ -94,6 +115,10 @@ namespace DVMConsole
return inputDevices; return inputDevices;
} }
/// <summary>
///
/// </summary>
/// <returns></returns>
private List<string> GetAudioOutputDevices() private List<string> GetAudioOutputDevices()
{ {
List<string> outputDevices = new List<string>(); List<string> outputDevices = new List<string>();
@ -107,25 +132,35 @@ namespace DVMConsole
return outputDevices; return outputDevices;
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SaveButton_Click(object sender, RoutedEventArgs e) private void SaveButton_Click(object sender, RoutedEventArgs e)
{ {
int selectedInputIndex = InputDeviceComboBox.SelectedIndex; 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); settingsManager.UpdateChannelOutputDevice(entry.Key, entry.Value);
_audioManager.SetTalkgroupOutputDevice(entry.Key, entry.Value); audioManager.SetTalkgroupOutputDevice(entry.Key, entry.Value);
} }
DialogResult = true; DialogResult = true;
Close(); Close();
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void CancelButton_Click(object sender, RoutedEventArgs e) private void CancelButton_Click(object sender, RoutedEventArgs e)
{ {
DialogResult = false; DialogResult = false;
Close(); Close();
} }
} } // public partial class AudioSettingsWindow : Window
} } // namespace dvmconsole

@ -1,9 +1,9 @@
<Window x:Class="DVMConsole.CallHistoryWindow" <Window x:Class="dvmconsole.CallHistoryWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DVMConsole" xmlns:local="clr-namespace:dvmconsole"
mc:Ignorable="d" mc:Ignorable="d"
Title="Call History" Height="450" Width="351"> Title="Call History" Height="450" Width="351">
<Grid> <Grid>

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2025 Caleb, K4PHP * Copyright (C) 2025 Caleb, K4PHP
@ -15,12 +15,91 @@ using System.Collections.ObjectModel;
using System.Windows; using System.Windows;
using System.Windows.Media; using System.Windows.Media;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary>
///
/// </summary>
public class CallHistoryViewModel
{
/*
** Properties
*/
/// <summary>
///
/// </summary>
public ObservableCollection<CallEntry> CallHistory { get; set; }
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="CallHistoryViewModel"/> class.
/// </summary>
public CallHistoryViewModel()
{
CallHistory = new ObservableCollection<CallEntry>();
}
} // public class CallHistoryViewModel
/// <summary>
///
/// </summary>
public class CallEntry : DependencyObject
{
public static readonly DependencyProperty BackgroundColorProperty =
DependencyProperty.Register(nameof(BackgroundColor), typeof(Brush), typeof(CallEntry), new PropertyMetadata(Brushes.Transparent));
/*
** Properties
*/
/// <summary>
///
/// </summary>
public string Channel { get; set; }
/// <summary>
///
/// </summary>
public int SrcId { get; set; }
/// <summary>
///
/// </summary>
public int DstId { get; set; }
/// <summary>
///
/// </summary>
public Brush BackgroundColor
{
get { return (Brush)GetValue(BackgroundColorProperty); }
set { SetValue(BackgroundColorProperty, value); }
}
} // public class CallEntry : DependencyObject
/// <summary>
/// Interaction logic for CallHistoryWindow.xaml.
/// </summary>
public partial class CallHistoryWindow : Window public partial class CallHistoryWindow : Window
{ {
/*
** Properties
*/
/// <summary>
///
/// </summary>
public CallHistoryViewModel ViewModel { get; set; } public CallHistoryViewModel ViewModel { get; set; }
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="CallHistoryWindow"/> class.
/// </summary>
public CallHistoryWindow() public CallHistoryWindow()
{ {
InitializeComponent(); InitializeComponent();
@ -28,12 +107,22 @@ namespace DVMConsole
DataContext = ViewModel; DataContext = ViewModel;
} }
/// <summary>
///
/// </summary>
/// <param name="e"></param>
protected override void OnClosing(System.ComponentModel.CancelEventArgs e) protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{ {
e.Cancel = true; e.Cancel = true;
this.Hide(); this.Hide();
} }
/// <summary>
///
/// </summary>
/// <param name="channel"></param>
/// <param name="srcId"></param>
/// <param name="dstId"></param>
public void AddCall(string channel, int srcId, int dstId) public void AddCall(string channel, int srcId, int dstId)
{ {
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
@ -48,6 +137,12 @@ namespace DVMConsole
}); });
} }
/// <summary>
///
/// </summary>
/// <param name="channel"></param>
/// <param name="srcId"></param>
/// <param name="encrypted"></param>
public void ChannelKeyed(string channel, int srcId, bool encrypted) public void ChannelKeyed(string channel, int srcId, bool encrypted)
{ {
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
@ -63,6 +158,11 @@ namespace DVMConsole
}); });
} }
/// <summary>
///
/// </summary>
/// <param name="channel"></param>
/// <param name="srcId"></param>
public void ChannelUnkeyed(string channel, int srcId) public void ChannelUnkeyed(string channel, int srcId)
{ {
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
@ -73,31 +173,5 @@ namespace DVMConsole
} }
}); });
} }
} } // public partial class CallHistoryWindow : Window
} // namespace dvmconsole
public class CallHistoryViewModel
{
public ObservableCollection<CallEntry> CallHistory { get; set; }
public CallHistoryViewModel()
{
CallHistory = new ObservableCollection<CallEntry>();
}
}
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); }
}
}
}

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

@ -1,94 +1,206 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2024-2025 Caleb, K4PHP * Copyright (C) 2024-2025 Caleb, K4PHP
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
* *
*/ */
using System.Security.Policy; using fnecore.P25;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary> /// <summary>
/// Codeplug object used project wide /// Codeplug object used to configure the console.
/// </summary> /// </summary>
public class Codeplug public class Codeplug
{ {
/*
** Properties
*/
/// <summary>
///
/// </summary>
public List<System> Systems { get; set; } public List<System> Systems { get; set; }
/// <summary>
///
/// </summary>
public List<Zone> Zones { get; set; } public List<Zone> Zones { get; set; }
/*
** Classes
*/
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public class System public class System
{ {
/*
** Properties
*/
/// <summary>
///
/// </summary>
public string Name { get; set; } public string Name { get; set; }
/// <summary>
///
/// </summary>
public string Identity { get; set; } public string Identity { get; set; }
/// <summary>
///
/// </summary>
public string Address { get; set; } public string Address { get; set; }
/// <summary>
///
/// </summary>
public string Password { get; set; } public string Password { get; set; }
/// <summary>
///
/// </summary>
public string PresharedKey { get; set; } public string PresharedKey { get; set; }
/// <summary>
///
/// </summary>
public bool Encrypted { get; set; } public bool Encrypted { get; set; }
/// <summary>
///
/// </summary>
public uint PeerId { get; set; } public uint PeerId { get; set; }
/// <summary>
///
/// </summary>
public int Port { get; set; } public int Port { get; set; }
/// <summary>
///
/// </summary>
public string Rid { get; set; } public string Rid { get; set; }
/// <summary>
///
/// </summary>
public string AliasPath { get; set; } = "./alias.yml"; public string AliasPath { get; set; } = "./alias.yml";
/// <summary>
///
/// </summary>
public List<RadioAlias> RidAlias { get; set; } = null; public List<RadioAlias> RidAlias { get; set; } = null;
/*
** Methods
*/
/// <summary>
///
/// </summary>
/// <returns></returns>
public override string ToString() public override string ToString()
{ {
return Name; return Name;
} }
} } // public class System
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public class Zone public class Zone
{ {
/*
** Properties
*/
/// <summary>
///
/// </summary>
public string Name { get; set; } public string Name { get; set; }
/// <summary>
///
/// </summary>
public List<Channel> Channels { get; set; } public List<Channel> Channels { get; set; }
} } // public class Zone
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public class Channel public class Channel
{ {
/*
** Properties
*/
/// <summary>
///
/// </summary>
public string Name { get; set; } public string Name { get; set; }
/// <summary>
///
/// </summary>
public string System { get; set; } public string System { get; set; }
/// <summary>
///
/// </summary>
public string Tgid { get; set; } public string Tgid { get; set; }
/// <summary>
///
/// </summary>
public string EncryptionKey { get; set; } public string EncryptionKey { get; set; }
public string AlgoId { get; set; } = "0x80"; /// <summary>
///
/// </summary>
public string Algo { get; set; } = "none";
/// <summary>
///
/// </summary>
public string KeyId { get; set; } public string KeyId { get; set; }
/*
** Methods
*/
/// <summary>
///
/// </summary>
/// <returns></returns>
public ushort GetKeyId() public ushort GetKeyId()
{ {
return Convert.ToUInt16(KeyId, 16); return Convert.ToUInt16(KeyId, 16);
} }
/// <summary>
///
/// </summary>
/// <returns></returns>
public byte GetAlgoId() 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;
}
} }
/// <summary>
///
/// </summary>
/// <returns></returns>
public byte[] GetEncryptionKey() public byte[] GetEncryptionKey()
{ {
if (EncryptionKey == null) if (EncryptionKey == null)
return []; return [];
return EncryptionKey return EncryptionKey.Split(',').Select(s => Convert.ToByte(s.Trim(), 16)).ToArray();
.Split(',')
.Select(s => Convert.ToByte(s.Trim(), 16))
.ToArray();
} }
} } // public class Channel
/// <summary> /// <summary>
/// Helper to return a system by looking up a <see cref="Channel"/> /// Helper to return a system by looking up a <see cref="Channel"/>
@ -111,10 +223,9 @@ namespace DVMConsole
{ {
var channel = zone.Channels.FirstOrDefault(c => c.Name == channelName); var channel = zone.Channels.FirstOrDefault(c => c.Name == channelName);
if (channel != null) if (channel != null)
{
return Systems.FirstOrDefault(s => s.Name == channel.System); return Systems.FirstOrDefault(s => s.Name == channel.System);
}
} }
return null; return null;
} }
@ -129,11 +240,10 @@ namespace DVMConsole
{ {
var channel = zone.Channels.FirstOrDefault(c => c.Name == channelName); var channel = zone.Channels.FirstOrDefault(c => c.Name == channelName);
if (channel != null) if (channel != null)
{
return channel; return channel;
}
} }
return null; return null;
} }
} } //public class Codeplug
} } // namespace dvmconsole

@ -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.");
}
}
}

@ -9,6 +9,9 @@
<Platforms>AnyCPU;x64;x86</Platforms> <Platforms>AnyCPU;x64;x86</Platforms>
<Configurations>Debug;Release;WIN32</Configurations> <Configurations>Debug;Release;WIN32</Configurations>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Copyright>Copyright (c) 2025 Caleb, K4PHP and DVMProject (https://github.com/dvmproject) Authors.</Copyright>
<PlatformTarget>x86</PlatformTarget>
<PackageLicenseExpression>AGPL-3.0-only</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -28,7 +31,6 @@
<None Remove="Assets\pttselect.png" /> <None Remove="Assets\pttselect.png" />
<None Remove="Assets\whackerlink-logo.png" /> <None Remove="Assets\whackerlink-logo.png" />
<None Remove="Assets\WhackerLinkLogoV2.png" /> <None Remove="Assets\WhackerLinkLogoV2.png" />
<None Remove="Assets\WhackerLinkLogoV4.png" />
<None Remove="clearemerg.png" /> <None Remove="clearemerg.png" />
<None Remove="connection.png" /> <None Remove="connection.png" />
<None Remove="page.png" /> <None Remove="page.png" />
@ -94,9 +96,6 @@
<Content Include="Assets\pttselect.png"> <Content Include="Assets\pttselect.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Assets\WhackerLinkLogoV4.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -121,7 +120,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="ChannelBox.xaml"> <Page Update="Controls\ChannelBox.xaml">
<CopyToOutputDirectory></CopyToOutputDirectory> <CopyToOutputDirectory></CopyToOutputDirectory>
</Page> </Page>
</ItemGroup> </ItemGroup>

@ -1,9 +1,9 @@
<Window x:Class="DVMConsole.DigitalPageWindow" <Window x:Class="dvmconsole.DigitalPageWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WhackerLinkConsoleV2" xmlns:local="clr-namespace:dvmconsole"
mc:Ignorable="d" mc:Ignorable="d"
Title="P25 Page" Height="200" Width="300" WindowStartupLocation="CenterOwner"> Title="P25 Page" Height="200" Width="300" WindowStartupLocation="CenterOwner">
<Grid> <Grid>

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2025 Caleb, K4PHP * Copyright (C) 2025 Caleb, K4PHP
@ -13,10 +13,10 @@
using System.Windows; using System.Windows;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary> /// <summary>
/// Interaction logic for DigitalPageWindow.xaml /// Interaction logic for DigitalPageWindow.xaml.
/// </summary> /// </summary>
public partial class DigitalPageWindow : Window public partial class DigitalPageWindow : Window
{ {
@ -25,6 +25,10 @@ namespace DVMConsole
public string DstId = string.Empty; public string DstId = string.Empty;
public Codeplug.System RadioSystem = null; public Codeplug.System RadioSystem = null;
/// <summary>
/// Initializes a new instance of the <see cref="DigitalPageWindow"/> class.
/// </summary>
/// <param name="systems"></param>
public DigitalPageWindow(List<Codeplug.System> systems) public DigitalPageWindow(List<Codeplug.System> systems)
{ {
InitializeComponent(); InitializeComponent();
@ -35,6 +39,11 @@ namespace DVMConsole
SystemCombo.SelectedIndex = 0; SystemCombo.SelectedIndex = 0;
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SendPageButton_Click(object sender, RoutedEventArgs e) private void SendPageButton_Click(object sender, RoutedEventArgs e)
{ {
RadioSystem = SystemCombo.SelectedItem as Codeplug.System; RadioSystem = SystemCombo.SelectedItem as Codeplug.System;
@ -42,5 +51,5 @@ namespace DVMConsole
DialogResult = true; DialogResult = true;
Close(); Close();
} }
} } // public partial class DigitalPageWindow : Window
} } // namespace dvmconsole

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2025 Caleb, K4PHP * Copyright (C) 2025 Caleb, K4PHP
@ -16,96 +16,125 @@ using System.Windows.Controls;
using System.Windows.Media; using System.Windows.Media;
using System.Windows.Threading; using System.Windows.Threading;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary>
///
/// </summary>
public class FlashingBackgroundManager public class FlashingBackgroundManager
{ {
private readonly Control _control; private readonly Control control;
private readonly Canvas _canvas; private readonly Canvas canvas;
private readonly UserControl _userControl; private readonly UserControl userControl;
private readonly Window _mainWindow; private readonly Window mainWindow;
private readonly DispatcherTimer _timer; private readonly DispatcherTimer timer;
private Brush _originalControlBackground;
private Brush _originalCanvasBackground; private Brush originalControlBackground;
private Brush _originalUserControlBackground; private Brush originalCanvasBackground;
private Brush _originalMainWindowBackground; private Brush originalUserControlBackground;
private bool _isFlashing; private Brush originalMainWindowBackground;
private bool isFlashing;
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="FlashingBackgroundManager"/> class.
/// </summary>
/// <param name="control"></param>
/// <param name="canvas"></param>
/// <param name="userControl"></param>
/// <param name="mainWindow"></param>
/// <param name="intervalMilliseconds"></param>
/// <exception cref="ArgumentException"></exception>
public FlashingBackgroundManager(Control control = null, Canvas canvas = null, UserControl userControl = null, Window mainWindow = null, int intervalMilliseconds = 450) public FlashingBackgroundManager(Control control = null, Canvas canvas = null, UserControl userControl = null, Window mainWindow = null, int intervalMilliseconds = 450)
{ {
_control = control; this.control = control;
_canvas = canvas; this.canvas = canvas;
_userControl = userControl; this.userControl = userControl;
_mainWindow = mainWindow; 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."); 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) Interval = TimeSpan.FromMilliseconds(intervalMilliseconds)
}; };
_timer.Tick += OnTimerTick; timer.Tick += OnTimerTick;
} }
/// <summary>
///
/// </summary>
public void Start() public void Start()
{ {
if (_isFlashing) if (isFlashing)
return; return;
if (_control != null) if (control != null)
_originalControlBackground = _control.Background; originalControlBackground = control.Background;
if (_canvas != null) if (canvas != null)
_originalCanvasBackground = _canvas.Background; originalCanvasBackground = canvas.Background;
if (_userControl != null) if (userControl != null)
_originalUserControlBackground = _userControl.Background; originalUserControlBackground = userControl.Background;
if (_mainWindow != null) if (mainWindow != null)
_originalMainWindowBackground = _mainWindow.Background; originalMainWindowBackground = mainWindow.Background;
_isFlashing = true; isFlashing = true;
_timer.Start(); timer.Start();
} }
/// <summary>
///
/// </summary>
public void Stop() public void Stop()
{ {
if (!_isFlashing) if (!isFlashing)
return; return;
_timer.Stop(); timer.Stop();
if (_control != null) if (control != null)
_control.Background = _originalControlBackground; control.Background = originalControlBackground;
if (_canvas != null) if (canvas != null)
_canvas.Background = _originalCanvasBackground; canvas.Background = originalCanvasBackground;
if (_userControl != null) if (userControl != null)
_userControl.Background = _originalUserControlBackground; userControl.Background = originalUserControlBackground;
if (_mainWindow != null && _originalMainWindowBackground != null) if (mainWindow != null && originalMainWindowBackground != null)
_mainWindow.Background = _originalMainWindowBackground; mainWindow.Background = originalMainWindowBackground;
_isFlashing = false; isFlashing = false;
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnTimerTick(object sender, EventArgs e) private void OnTimerTick(object sender, EventArgs e)
{ {
Brush flashingColor = Brushes.Red; Brush flashingColor = Brushes.Red;
if (_control != null) if (control != null)
_control.Background = _control.Background == Brushes.DarkRed ? _originalControlBackground : Brushes.DarkRed; control.Background = control.Background == Brushes.DarkRed ? originalControlBackground : Brushes.DarkRed;
if (_canvas != null) if (canvas != null)
_canvas.Background = _canvas.Background == flashingColor ? _originalCanvasBackground : flashingColor; canvas.Background = canvas.Background == flashingColor ? originalCanvasBackground : flashingColor;
if (_userControl != null) if (userControl != null)
_userControl.Background = _userControl.Background == Brushes.DarkRed ? _originalUserControlBackground : Brushes.DarkRed; userControl.Background = userControl.Background == Brushes.DarkRed ? originalUserControlBackground : Brushes.DarkRed;
if (_mainWindow != null) if (mainWindow != null)
_mainWindow.Background = _mainWindow.Background == flashingColor ? _originalMainWindowBackground : flashingColor; mainWindow.Background = mainWindow.Background == flashingColor ? originalMainWindowBackground : flashingColor;
} }
} } // public class FlashingBackgroundManager
} } // namespace dvmconsole

@ -11,12 +11,12 @@
* *
*/ */
using fnecore.DMR;
using fnecore; using fnecore;
using fnecore.DMR;
using NAudio.Wave; using NAudio.Wave;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary> /// <summary>
/// Implements a FNE system base. /// Implements a FNE system base.
@ -113,4 +113,4 @@ namespace DVMConsole
return; return;
} }
} // public abstract partial class FneSystemBase : fnecore.FneSystemBase } // public abstract partial class FneSystemBase : fnecore.FneSystemBase
} } // namespace dvmconsole

@ -11,10 +11,10 @@
* *
*/ */
using fnecore.NXDN;
using fnecore; using fnecore;
using fnecore.NXDN;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary> /// <summary>
/// Implements a FNE system base. /// Implements a FNE system base.
@ -54,4 +54,4 @@ namespace DVMConsole
return; return;
} }
} // public abstract partial class FneSystemBase : fnecore.FneSystemBase } // public abstract partial class FneSystemBase : fnecore.FneSystemBase
} } // namespace dvmconsole

@ -15,8 +15,31 @@
using fnecore; using fnecore;
using fnecore.P25; using fnecore.P25;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary>
///
/// </summary>
public class CryptoParams
{
/*
** Properties
*/
/// <summary>
/// Message Indicator
/// </summary>
public byte[] MI { get; set; } = new byte[P25Defines.P25_MI_LENGTH];
/// <summary>
/// Algorithm ID.
/// </summary>
public byte AlgId { get; set; } = P25Defines.P25_ALGO_UNENCRYPT;
/// <summary>
/// Key ID.
/// </summary>
public ushort KeyId { get; set; }
} // public class CryptoParams
/// <summary> /// <summary>
/// Implements a FNE system base. /// Implements a FNE system base.
/// </summary> /// </summary>
@ -55,11 +78,20 @@ namespace DVMConsole
return; return;
} }
/// <summary>
///
/// </summary>
/// <param name="duid"></param>
/// <param name="callData"></param>
/// <param name="data"></param>
/// <param name="algId"></param>
/// <param name="kId"></param>
/// <param name="mi"></param>
public void CreateNewP25MessageHdr(byte duid, RemoteCallData callData, ref byte[] data, byte algId = 0, ushort kId = 0, byte[] mi = null) public void CreateNewP25MessageHdr(byte duid, RemoteCallData callData, ref byte[] data, byte algId = 0, ushort kId = 0, byte[] mi = null)
{ {
CreateP25MessageHdr(duid, callData, ref data); 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) if (mi != null)
{ {
data[14U] |= 0x08; // Control bit data[14U] |= 0x08; // Control bit
@ -329,25 +361,25 @@ namespace DVMConsole
break; break;
case P25DFSI.P25_DFSI_LDU2_VOICE12: case P25DFSI.P25_DFSI_LDU2_VOICE12:
{ {
dfsiFrame[1U] = cryptoParams.Mi[0]; // Message Indicator dfsiFrame[1U] = cryptoParams.MI[0]; // Message Indicator
dfsiFrame[2U] = cryptoParams.Mi[1]; dfsiFrame[2U] = cryptoParams.MI[1];
dfsiFrame[3U] = cryptoParams.Mi[2]; dfsiFrame[3U] = cryptoParams.MI[2];
Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE
} }
break; break;
case P25DFSI.P25_DFSI_LDU2_VOICE13: case P25DFSI.P25_DFSI_LDU2_VOICE13:
{ {
dfsiFrame[1U] = cryptoParams.Mi[3]; // Message Indicator dfsiFrame[1U] = cryptoParams.MI[3]; // Message Indicator
dfsiFrame[2U] = cryptoParams.Mi[4]; dfsiFrame[2U] = cryptoParams.MI[4];
dfsiFrame[3U] = cryptoParams.Mi[5]; dfsiFrame[3U] = cryptoParams.MI[5];
Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE
} }
break; break;
case P25DFSI.P25_DFSI_LDU2_VOICE14: case P25DFSI.P25_DFSI_LDU2_VOICE14:
{ {
dfsiFrame[1U] = cryptoParams.Mi[6]; // Message Indicator dfsiFrame[1U] = cryptoParams.MI[6]; // Message Indicator
dfsiFrame[2U] = cryptoParams.Mi[7]; dfsiFrame[2U] = cryptoParams.MI[7];
dfsiFrame[3U] = cryptoParams.Mi[8]; dfsiFrame[3U] = cryptoParams.MI[8];
Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE
} }
break; break;
@ -468,14 +500,4 @@ namespace DVMConsole
} }
} }
} // public abstract partial class FneSystemBase : fnecore.FneSystemBase } // public abstract partial class FneSystemBase : fnecore.FneSystemBase
} // namespace dvmconsole
/// <summary>
///
/// </summary>
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; }
}
}

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2022-2024 Bryan Biedenkapp, N2PLL * Copyright (C) 2022-2024 Bryan Biedenkapp, N2PLL
@ -12,11 +12,11 @@
* *
*/ */
using fnecore.DMR;
using fnecore; using fnecore;
using fnecore.DMR;
using fnecore.P25.kmm; using fnecore.P25.kmm;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary> /// <summary>
/// Represents the individual timeslot data status. /// Represents the individual timeslot data status.
@ -196,11 +196,8 @@ namespace DVMConsole
byte[] payload = e.Data.Skip(11).ToArray(); byte[] payload = e.Data.Skip(11).ToArray();
//Console.WriteLine(FneUtils.HexDump(payload)); //Console.WriteLine(FneUtils.HexDump(payload));
if (e.MessageId == (byte)KmmMessageType.MODIFY_KEY_CMD) if (e.MessageId == (byte)KmmMessageType.MODIFY_KEY_CMD)
{
mainWindow.KeyResponseReceived(e); mainWindow.KeyResponseReceived(e);
}
} }
/// <summary> /// <summary>
@ -213,4 +210,4 @@ namespace DVMConsole
} }
} // public abstract partial class FneSystemBase : fnecore.FneSystemBase } // public abstract partial class FneSystemBase : fnecore.FneSystemBase
} } // namespace dvmconsole

@ -1,31 +1,35 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2025 Caleb, K4PHP * Copyright (C) 2025 Caleb, K4PHP
* *
*/ */
namespace DVMConsole namespace dvmconsole
{ {
/// <summary> /// <summary>
/// WhackerLink peer/client websocket manager for having multiple systems ///
/// </summary> /// </summary>
public class FneSystemManager public class FneSystemManager
{ {
private readonly Dictionary<string, PeerSystem> _webSocketHandlers; private readonly Dictionary<string, PeerSystem> peerHandlers;
/*
** Methods
*/
/// <summary> /// <summary>
/// Creates an instance of <see cref="PeerSystem"/> /// Creates an instance of <see cref="PeerSystem"/> class.
/// </summary> /// </summary>
public FneSystemManager() public FneSystemManager()
{ {
_webSocketHandlers = new Dictionary<string, PeerSystem>(); peerHandlers = new Dictionary<string, PeerSystem>();
} }
/// <summary> /// <summary>
@ -34,10 +38,8 @@ namespace DVMConsole
/// <param name="systemId"></param> /// <param name="systemId"></param>
public void AddFneSystem(string systemId, Codeplug.System system, MainWindow mainWindow) public void AddFneSystem(string systemId, Codeplug.System system, MainWindow mainWindow)
{ {
if (!_webSocketHandlers.ContainsKey(systemId)) if (!peerHandlers.ContainsKey(systemId))
{ peerHandlers[systemId] = new PeerSystem(mainWindow, system);
_webSocketHandlers[systemId] = new PeerSystem(mainWindow, system);
}
} }
/// <summary> /// <summary>
@ -48,10 +50,9 @@ namespace DVMConsole
/// <exception cref="KeyNotFoundException"></exception> /// <exception cref="KeyNotFoundException"></exception>
public PeerSystem GetFneSystem(string systemId) public PeerSystem GetFneSystem(string systemId)
{ {
if (_webSocketHandlers.TryGetValue(systemId, out var handler)) if (peerHandlers.TryGetValue(systemId, out var handler))
{
return handler; return handler;
}
throw new KeyNotFoundException($"WebSocketHandler for system '{systemId}' not found."); throw new KeyNotFoundException($"WebSocketHandler for system '{systemId}' not found.");
} }
@ -61,10 +62,10 @@ namespace DVMConsole
/// <param name="systemId"></param> /// <param name="systemId"></param>
public void RemoveFneSystem(string systemId) public void RemoveFneSystem(string systemId)
{ {
if (_webSocketHandlers.TryGetValue(systemId, out var handler)) if (peerHandlers.TryGetValue(systemId, out var handler))
{ {
handler.peer.Stop(); handler.peer.Stop();
_webSocketHandlers.Remove(systemId); peerHandlers.Remove(systemId);
} }
} }
@ -75,7 +76,7 @@ namespace DVMConsole
/// <returns></returns> /// <returns></returns>
public bool HasFneSystem(string systemId) public bool HasFneSystem(string systemId)
{ {
return _webSocketHandlers.ContainsKey(systemId); return peerHandlers.ContainsKey(systemId);
} }
/// <summary> /// <summary>
@ -83,11 +84,10 @@ namespace DVMConsole
/// </summary> /// </summary>
public void ClearAll() public void ClearAll()
{ {
foreach (var handler in _webSocketHandlers.Values) foreach (var handler in peerHandlers.Values)
{
handler.peer.Stop(); handler.peer.Stop();
}
_webSocketHandlers.Clear(); peerHandlers.Clear();
} }
} } // public class FneSystemManager
} } // namespace dvmconsole

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2025 Caleb, K4PHP * Copyright (C) 2025 Caleb, K4PHP
@ -12,40 +12,64 @@
*/ */
using NAudio.Wave; using NAudio.Wave;
using NAudio.Wave.SampleProviders;
using System;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary>
///
/// </summary>
public class GainSampleProvider : ISampleProvider public class GainSampleProvider : ISampleProvider
{ {
private readonly ISampleProvider _source; private readonly ISampleProvider source;
private float _gain = 1.0f; private float gain = 1.0f;
public GainSampleProvider(ISampleProvider source) /*
{ ** Properties
_source = source ?? throw new ArgumentNullException(nameof(source)); */
WaveFormat = source.WaveFormat;
}
/// <summary>
///
/// </summary>
public WaveFormat WaveFormat { get; } public WaveFormat WaveFormat { get; }
/// <summary>
///
/// </summary>
public float Gain public float Gain
{ {
get => _gain; get => gain;
set => _gain = Math.Max(0, value); set => gain = Math.Max(0, value);
} }
public int Read(float[] buffer, int offset, int count) /*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="GainSampleProvider"/> class.
/// </summary>
/// <param name="source"></param>
/// <exception cref="ArgumentNullException"></exception>
public GainSampleProvider(ISampleProvider source)
{ {
int samplesRead = _source.Read(buffer, offset, count); this.source = source ?? throw new ArgumentNullException(nameof(source));
WaveFormat = source.WaveFormat;
}
/// <summary>
///
/// </summary>
/// <param name="buffer"></param>
/// <param name="offset"></param>
/// <param name="count"></param>
/// <returns></returns>
public int Read(float[] buffer, int offset, int count)
{
int samplesRead = source.Read(buffer, offset, count);
for (int i = 0; i < samplesRead; i++) for (int i = 0; i < samplesRead; i++)
{ buffer[offset + i] *= gain;
buffer[offset + i] *= _gain;
}
return samplesRead; return samplesRead;
} }
} } // public class GainSampleProvider : ISampleProvider
} } // namespace dvmconsole

@ -1,4 +1,4 @@
<Window x:Class="DVMConsole.KeyStatusWindow" <Window x:Class="dvmconsole.KeyStatusWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2025 Caleb, K4PHP * Copyright (C) 2025 Caleb, K4PHP
@ -12,18 +12,66 @@
*/ */
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows; using System.Windows;
using DVMConsole.Controls;
namespace DVMConsole using dvmconsole.Controls;
namespace dvmconsole
{ {
public partial class KeyStatusWindow : Window /// <summary>
///
/// </summary>
public class KeyStatusItem
{ {
public ObservableCollection<KeyStatusItem> KeyStatusItems { get; private set; } = new ObservableCollection<KeyStatusItem>(); /*
** Properties
*/
/// <summary>
///
/// </summary>
public string ChannelName { get; set; }
/// <summary>
///
/// </summary>
public string AlgId { get; set; }
/// <summary>
///
/// </summary>
public string KeyId { get; set; }
/// <summary>
///
/// </summary>
public string KeyStatus { get; set; }
} // public class KeyStatusItem
/// <summary>
///
/// </summary>
public partial class KeyStatusWindow : Window
{
private Codeplug Codeplug; private Codeplug Codeplug;
private MainWindow mainWindow; private MainWindow mainWindow;
/*
** Properties
*/
/// <summary>
///
/// </summary>
public ObservableCollection<KeyStatusItem> KeyStatusItems { get; private set; } = new ObservableCollection<KeyStatusItem>();
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="KeyStatusWindow"/> class.
/// </summary>
/// <param name="codeplug"></param>
/// <param name="mainWindow"></param>
public KeyStatusWindow(Codeplug codeplug, MainWindow mainWindow) public KeyStatusWindow(Codeplug codeplug, MainWindow mainWindow)
{ {
InitializeComponent(); InitializeComponent();
@ -34,6 +82,9 @@ namespace DVMConsole
LoadKeyStatus(); LoadKeyStatus();
} }
/// <summary>
///
/// </summary>
private void LoadKeyStatus() private void LoadKeyStatus()
{ {
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
@ -44,7 +95,7 @@ namespace DVMConsole
{ {
if (child == null) if (child == null)
{ {
Console.WriteLine("A child in ChannelsCanvas.Children is null."); Trace.WriteLine("A child in ChannelsCanvas.Children is null.");
continue; continue;
} }
@ -56,14 +107,14 @@ namespace DVMConsole
Codeplug.System system = Codeplug.GetSystemForChannel(channelBox.ChannelName); Codeplug.System system = Codeplug.GetSystemForChannel(channelBox.ChannelName);
if (system == null) if (system == null)
{ {
Console.WriteLine($"System not found for {channelBox.ChannelName}"); Trace.WriteLine($"System not found for {channelBox.ChannelName}");
continue; continue;
} }
Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channelBox.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channelBox.ChannelName);
if (cpgChannel == null) if (cpgChannel == null)
{ {
Console.WriteLine($"Channel not found for {channelBox.ChannelName}"); Trace.WriteLine($"Channel not found for {channelBox.ChannelName}");
continue; continue;
} }
@ -72,7 +123,7 @@ namespace DVMConsole
if (channelBox.crypter == null) if (channelBox.crypter == null)
{ {
Console.WriteLine($"Crypter is null for channel {channelBox.ChannelName}"); Trace.WriteLine($"Crypter is null for channel {channelBox.ChannelName}");
continue; continue;
} }
@ -88,13 +139,5 @@ namespace DVMConsole
} }
}); });
} }
} } // public partial class KeyStatusWindow : Window
} // 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; }
}
}

@ -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.Signals;
using NWaves.Transforms; using NWaves.Transforms;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary>
///
/// </summary>
public class MBEToneDetector public class MBEToneDetector
{ {
// Samplerate is 8000 Hz // Samplerate is 8000 Hz
@ -41,6 +50,10 @@ namespace DVMConsole
// The STFT (short-time fourier transform) operator // The STFT (short-time fourier transform) operator
private Stft stft; private Stft stft;
/*
** Methods
*/
/// <summary> /// <summary>
/// Create a pitch detector which reports the running average of pitch for a sequence of samples /// Create a pitch detector which reports the running average of pitch for a sequence of samples
/// </summary> /// </summary>
@ -123,5 +136,5 @@ namespace DVMConsole
} }
return 0; return 0;
} }
} } // public class MBEToneDetector
} } // namespace dvmconsole

@ -1,8 +1,8 @@
<Window x:Class="DVMConsole.MainWindow" <Window x:Class="dvmconsole.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DVMConsole.Controls" xmlns:local="clr-namespace:dvmconsole.Controls"
Title="DVMConsole" Height="600" Width="1000" Background="#FFF2F2F2"> Title="Digital Voice Modem - Desktop Dispatch Console" Height="600" Width="1000" Background="#FFF2F2F2">
<Grid Background="#FFF2F2F2"> <Grid Background="#FFF2F2F2">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="497*"/> <ColumnDefinition Width="497*"/>

File diff suppressed because it is too large Load Diff

@ -1,51 +1,104 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2025 Caleb, K4PHP * Copyright (C) 2025 Caleb, K4PHP
* *
*/ */
// TODO: Move to fnecore
using System;
using System.Collections.Generic;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Linq;
namespace DVMConsole using fnecore.P25;
namespace dvmconsole
{ {
/// <summary>
///
/// </summary>
public class P25Crypto public class P25Crypto
{ {
private ProtocolType protocol; public const int IMBE_BUF_LEN = 11;
private byte algId; private byte algId;
private ushort keyId; private ushort keyId;
private byte[] messageIndicator = new byte[9]; private byte[] messageIndicator = new byte[9];
private Dictionary<ushort, KeyInfo> keys = new Dictionary<ushort, KeyInfo>(); private Dictionary<ushort, KeyInfo> keys = new Dictionary<ushort, KeyInfo>();
private byte[] aesKeystream = new byte[240]; // AES buffer private byte[] aesKeystream = new byte[240]; // AES buffer
private byte[] adpKeystream = new byte[469]; // ADP buffer private byte[] adpKeystream = new byte[469]; // ADP buffer
private int aesPosition;
private int adpPosition;
private int ksPosition;
/*
** Class
*/
/// <summary>
///
/// </summary>
private class KeyInfo
{
/*
** Properties
*/
/// <summary>
///
/// </summary>
public byte AlgId { get; }
/// <summary>
///
/// </summary>
public byte[] Key { get; }
/// <summary>
/// Initializes a new instance of the <see cref="KeyInfo"/> class.
/// </summary>
/// <param name="algid"></param>
/// <param name="key"></param>
public KeyInfo(byte algid, byte[] key)
{
AlgId = algid;
Key = key;
}
} // private class KeyInfo
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="P25Crypto"/> class.
/// </summary>
public P25Crypto() public P25Crypto()
{ {
this.protocol = ProtocolType.Unknown; this.algId = P25Defines.P25_ALGO_UNENCRYPT;
this.algId = 0x80;
this.keyId = 0; this.keyId = 0;
this.aesPosition = 0;
this.adpPosition = 0; this.ksPosition = 0;
} }
/// <summary>
///
/// </summary>
public void Reset() public void Reset()
{ {
keys.Clear(); keys.Clear();
} }
/// <summary>
///
/// </summary>
/// <param name="keyid"></param>
/// <param name="algid"></param>
/// <param name="key"></param>
public void AddKey(ushort keyid, byte algid, byte[] key) public void AddKey(ushort keyid, byte algid, byte[] key)
{ {
if (keyid == 0 || algid == 0x80) if (keyid == 0 || algid == 0x80)
@ -54,58 +107,90 @@ namespace DVMConsole
keys[keyid] = new KeyInfo(algid, key); keys[keyid] = new KeyInfo(algid, key);
} }
/// <summary>
///
/// </summary>
/// <param name="keyId"></param>
/// <returns></returns>
public bool HasKey(ushort keyId) public bool HasKey(ushort keyId)
{ {
return keys.ContainsKey(keyId); return keys.ContainsKey(keyId);
} }
public bool Prepare(byte algid, ushort keyid, ProtocolType protocol, byte[] MI) /// <summary>
///
/// </summary>
/// <param name="algid"></param>
/// <param name="keyid"></param>
/// <param name="protocol"></param>
/// <param name="MI"></param>
/// <returns></returns>
public bool Prepare(byte algid, ushort keyid, byte[] MI)
{ {
this.algId = algid; this.algId = algid;
this.keyId = keyid; this.keyId = keyid;
this.protocol = protocol;
Array.Copy(MI, this.messageIndicator, Math.Min(MI.Length, this.messageIndicator.Length)); Array.Copy(MI, this.messageIndicator, Math.Min(MI.Length, this.messageIndicator.Length));
if (!keys.ContainsKey(keyid)) if (!keys.ContainsKey(keyid))
return false; return false;
if (algid == 0x84) // AES-256 this.ksPosition = 0;
if (algid == P25Defines.P25_ALGO_AES)
{ {
this.aesPosition = 0; GenerateAESKeystream();
GenerateAesKeystream();
return true; return true;
} }
else if (algid == 0xAA) // ADP (RC4) else if (algid == P25Defines.P25_ALGO_ARC4)
{ {
this.adpPosition = 0; GenerateARC4Keystream();
GenerateAdpKeystream();
return true; return true;
} }
return false; return false;
} }
public bool Process(byte[] PCW, FrameType frameType, int voiceSubframe) /// <summary>
///
/// </summary>
/// <param name="imbe"></param>
/// <param name="duid"></param>
/// <returns></returns>
public bool Process(byte[] imbe, P25DUID duid)
{ {
if (!keys.ContainsKey(keyId)) if (!keys.ContainsKey(keyId))
return false; return false;
return algId switch return algId switch
{ {
0x84 => AesProcess(PCW, frameType, voiceSubframe), P25Defines.P25_ALGO_AES => AESProcess(imbe, duid),
0xAA => AdpProcess(PCW, frameType, voiceSubframe), P25Defines.P25_ALGO_ARC4 => ARC4Process(imbe, duid),
_ => false _ => false
}; };
} }
/// <summary> /// <summary>
/// Create ADP key stream ///
/// </summary>
/// <param name="a"></param>
/// <param name="i1"></param>
/// <param name="i2"></param>
private void Swap(byte[] a, int i1, int i2)
{
byte temp = a[i1];
a[i1] = a[i2];
a[i2] = temp;
}
/// <summary>
/// Create ARC4 keystream.
/// </summary> /// </summary>
private void GenerateAdpKeystream() private void GenerateARC4Keystream()
{ {
byte[] adpKey = new byte[13]; byte[] adpKey = new byte[13];
byte[] S = new byte[256]; byte[] permutation = new byte[256];
byte[] K = new byte[256]; byte[] key = new byte[256];
if (!keys.ContainsKey(keyId)) if (!keys.ContainsKey(keyId))
return; return;
@ -127,141 +212,46 @@ namespace DVMConsole
adpKey[i] = messageIndicator[i - 5]; adpKey[i] = messageIndicator[i - 5];
} }
// generate ARC4 keystream
// initialize state variable
for (i = 0; i < 256; ++i) for (i = 0; i < 256; ++i)
{ {
K[i] = adpKey[i % 13]; key[i] = adpKey[i % 13];
S[i] = (byte)i; permutation[i] = (byte)i;
} }
// randomize, using key
for (i = 0; i < 256; ++i) for (i = 0; i < 256; ++i)
{ {
j = (j + S[i] + K[i]) & 0xFF; j = (j + permutation[i] + key[i]) & 0xFF;
Swap(S, i, j); Swap(permutation, i, j);
} }
// perform RC4 transformation
i = j = 0; i = j = 0;
for (k = 0; k < 469; ++k) for (k = 0; k < 469; ++k)
{ {
i = (i + 1) & 0xFF; i = (i + 1) & 0xFF;
j = (j + S[i]) & 0xFF; j = (j + permutation[i]) & 0xFF;
Swap(S, i, j);
adpKeystream[k] = S[(S[i] + S[j]) & 0xFF];
}
}
/// <summary>
/// Preform a swap
/// </summary>
/// <param name="S"></param>
/// <param name="i"></param>
/// <param name="j"></param>
private void Swap(byte[] S, int i, int j)
{
byte temp = S[i];
S[i] = S[j];
S[j] = temp;
}
/// <summary>
/// Process AES256
/// </summary>
/// <param name="PCW"></param>
/// <param name="frameType"></param>
/// <param name="voiceSubframe"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Process ADP
/// </summary>
/// <param name="PCW"></param>
/// <param name="frameType"></param>
/// <param name="voiceSubframe"></param>
/// <returns></returns>
private bool AdpProcess(byte[] PCW, FrameType frameType, int voiceSubframe)
{
int offset = 256;
switch (frameType) // swap permutation[i] and permutation[j]
{ Swap(permutation, i, j);
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) // transform byte
{ adpKeystream[k] = permutation[(permutation[i] + permutation[j]) & 0xFF];
offset += (adpPosition * 11) + 267 + (adpPosition < 8 ? 0 : 2);
adpPosition = (adpPosition + 1) % 9;
for (int j = 0; j < 11; ++j)
{
PCW[j] ^= adpKeystream[j + offset];
}
} }
else if (protocol == ProtocolType.P25Phase2)
{
for (int j = 0; j < 7; ++j)
{
PCW[j] ^= adpKeystream[j + offset];
}
PCW[6] &= 0x80;
}
return true;
} }
/// <summary> /// <summary>
/// Create AES key stream /// Create AES keystream.
/// </summary> /// </summary>
private void GenerateAesKeystream() private void GenerateAESKeystream()
{ {
if (!keys.ContainsKey(keyId)) if (!keys.ContainsKey(keyId))
return; return;
byte[] key = keys[keyId].Key; byte[] key = keys[keyId].Key;
byte[] iv = ExpandMiTo128(messageIndicator); byte[] iv = ExpandMIToIV(messageIndicator);
using (var aes = Aes.Create()) using (var aes = Aes.Create())
{ {
@ -287,6 +277,48 @@ namespace DVMConsole
} }
} }
/// <summary>
/// Helper to process IMBE audio using AES-256.
/// </summary>
/// <param name="imbe"></param>
/// <param name="duid"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Helper to process IMBE audio using ARC4.
/// </summary>
/// <param name="imbe"></param>
/// <param name="duid"></param>
/// <returns></returns>
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;
}
/// <summary> /// <summary>
/// Cycle P25 LFSR /// Cycle P25 LFSR
/// </summary> /// </summary>
@ -350,7 +382,7 @@ namespace DVMConsole
/// <param name="mi"></param> /// <param name="mi"></param>
/// <returns></returns> /// <returns></returns>
/// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentException"></exception>
private static byte[] ExpandMiTo128(byte[] mi) private static byte[] ExpandMIToIV(byte[] mi)
{ {
if (mi == null || mi.Length < 8) if (mi == null || mi.Length < 8)
throw new ArgumentException("MI must be at least 8 bytes long."); 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 // Copy first 64 bits of MI into LFSR
ulong lfsr = 0; ulong lfsr = 0;
for (int i = 0; i < 8; i++) for (int i = 0; i < 8; i++)
{
lfsr = (lfsr << 8) | mi[i]; lfsr = (lfsr << 8) | mi[i];
}
// Use LFSR routine to compute the expansion // Use LFSR routine to compute the expansion
ulong overflow = 0; ulong overflow = 0;
for (int i = 0; i < 64; i++) for (int i = 0; i < 64; i++)
{
overflow = (overflow << 1) | StepP25Lfsr(ref lfsr); overflow = (overflow << 1) | StepP25Lfsr(ref lfsr);
}
// Copy expansion and LFSR to IV // Copy expansion and LFSR to IV
for (int i = 7; i >= 0; i--) for (int i = 7; i >= 0; i--)
@ -377,6 +405,7 @@ namespace DVMConsole
iv[i] = (byte)(overflow & 0xFF); iv[i] = (byte)(overflow & 0xFF);
overflow >>= 8; overflow >>= 8;
} }
for (int i = 15; i >= 8; i--) for (int i = 15; i >= 8; i--)
{ {
iv[i] = (byte)(lfsr & 0xFF); iv[i] = (byte)(lfsr & 0xFF);
@ -385,37 +414,5 @@ namespace DVMConsole
return iv; return iv;
} }
} // public class P25Crypto
} // namespace dvmconsole
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
}
}
}

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2023 Bryan Biedenkapp, N2PLL * Copyright (C) 2023 Bryan Biedenkapp, N2PLL
@ -16,7 +16,7 @@ using System.Net;
using fnecore; using fnecore;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary> /// <summary>
/// Implements a peer FNE router system. /// Implements a peer FNE router system.
@ -113,4 +113,4 @@ namespace DVMConsole
/* stub */ /* stub */
} }
} // public class PeerSystem } // public class PeerSystem
} // namespace rc2_dvm } // namespace dvmconsole

@ -1,9 +1,9 @@
<Window x:Class="DVMConsole.QuickCallPage" <Window x:Class="dvmconsole.QuickCallPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DVMConsole" xmlns:local="clr-namespace:dvmconsole"
mc:Ignorable="d" mc:Ignorable="d"
Title="Manual QC2 Page" Height="300" Width="300"> Title="Manual QC2 Page" Height="300" Width="300">
<Grid> <Grid>

@ -1,26 +1,19 @@
/* // SPDX-License-Identifier: AGPL-3.0-only
* WhackerLink - 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.
* *
* This program is free software: you can redistribute it and/or modify * @package DVM / Desktop Dispatch Console
* it under the terms of the GNU General Public License as published by * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * Copyright (C) 2024 Caleb, K4PHP
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright (C) 2024 Caleb, K4PHP
*
*/ */
using System.Windows; using System.Windows;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary> /// <summary>
/// Interaction logic for QuickCallPage.xaml /// Interaction logic for QuickCallPage.xaml
@ -30,11 +23,23 @@ namespace DVMConsole
public string ToneA; public string ToneA;
public string ToneB; public string ToneB;
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="QuickCallPage"/> class.
/// </summary>
public QuickCallPage() public QuickCallPage()
{ {
InitializeComponent(); InitializeComponent();
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SendButton_Click(object sender, RoutedEventArgs e) private void SendButton_Click(object sender, RoutedEventArgs e)
{ {
ToneA = ToneAText.Text; ToneA = ToneAText.Text;
@ -43,5 +48,5 @@ namespace DVMConsole
DialogResult = true; DialogResult = true;
Close(); Close();
} }
} } // public partial class QuickCallPage : Window
} } // namespace dvmconsole

@ -13,7 +13,7 @@
using NAudio.Wave; using NAudio.Wave;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary> /// <summary>
/// ///
@ -67,5 +67,5 @@ namespace DVMConsole
{ {
return ToBytes(format, ToSamples(format, ms)); return ToBytes(format, ToSamples(format, ms));
} }
} // public class SamplesToMS } // public class SampleTimeConvert
} // namespace dvmbridge } // namespace dvmconsole

@ -1,59 +1,86 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2024 Caleb, K4PHP * Copyright (C) 2024 Caleb, K4PHP
* *
*/ */
using DVMConsole.Controls; using dvmconsole.Controls;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary>
///
/// </summary>
public class SelectedChannelsManager public class SelectedChannelsManager
{ {
private readonly HashSet<ChannelBox> _selectedChannels; private readonly HashSet<ChannelBox> selectedChannels;
public IReadOnlyCollection<ChannelBox> GetSelectedChannels() => selectedChannels;
/*
** Events
*/
/// <summary>
///
/// </summary>
public event Action SelectedChannelsChanged; public event Action SelectedChannelsChanged;
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="SelectedChannelsManager"/> class.
/// </summary>
public SelectedChannelsManager() public SelectedChannelsManager()
{ {
_selectedChannels = new HashSet<ChannelBox>(); selectedChannels = new HashSet<ChannelBox>();
} }
/// <summary>
///
/// </summary>
/// <param name="channel"></param>
public void AddSelectedChannel(ChannelBox channel) public void AddSelectedChannel(ChannelBox channel)
{ {
if (_selectedChannels.Add(channel)) if (selectedChannels.Add(channel))
{ {
channel.IsSelected = true; channel.IsSelected = true;
SelectedChannelsChanged.Invoke(); SelectedChannelsChanged.Invoke();
} }
} }
/// <summary>
///
/// </summary>
/// <param name="channel"></param>
public void RemoveSelectedChannel(ChannelBox channel) public void RemoveSelectedChannel(ChannelBox channel)
{ {
if (_selectedChannels.Remove(channel)) if (selectedChannels.Remove(channel))
{ {
channel.IsSelected = false; channel.IsSelected = false;
SelectedChannelsChanged.Invoke(); SelectedChannelsChanged.Invoke();
} }
} }
/// <summary>
///
/// </summary>
public void ClearSelections() public void ClearSelections()
{ {
foreach (var channel in _selectedChannels) foreach (var channel in selectedChannels)
{
channel.IsSelected = false; channel.IsSelected = false;
}
_selectedChannels.Clear(); selectedChannels.Clear();
SelectedChannelsChanged.Invoke(); SelectedChannelsChanged.Invoke();
} }
} // public class SelectedChannelsManager
public IReadOnlyCollection<ChannelBox> GetSelectedChannels() => _selectedChannels; } // namespace dvmconsole
}
}

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2024-2025 Caleb, K4PHP * Copyright (C) 2024-2025 Caleb, K4PHP
@ -12,26 +12,68 @@
*/ */
using System.IO; using System.IO;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary>
///
/// </summary>
public class SettingsManager public class SettingsManager
{ {
private const string SettingsFilePath = "UserSettings.json"; private const string SettingsFilePath = "UserSettings.json";
/*
** Properties
*/
/// <summary>
///
/// </summary>
public bool ShowSystemStatus { get; set; } = true; public bool ShowSystemStatus { get; set; } = true;
/// <summary>
///
/// </summary>
public bool ShowChannels { get; set; } = true; public bool ShowChannels { get; set; } = true;
/// <summary>
///
/// </summary>
public bool ShowAlertTones { get; set; } = true; public bool ShowAlertTones { get; set; } = true;
/// <summary>
///
/// </summary>
public string LastCodeplugPath { get; set; } = null; public string LastCodeplugPath { get; set; } = null;
/// <summary>
///
/// </summary>
public Dictionary<string, ChannelPosition> ChannelPositions { get; set; } = new Dictionary<string, ChannelPosition>(); public Dictionary<string, ChannelPosition> ChannelPositions { get; set; } = new Dictionary<string, ChannelPosition>();
/// <summary>
///
/// </summary>
public Dictionary<string, ChannelPosition> SystemStatusPositions { get; set; } = new Dictionary<string, ChannelPosition>(); public Dictionary<string, ChannelPosition> SystemStatusPositions { get; set; } = new Dictionary<string, ChannelPosition>();
/// <summary>
///
/// </summary>
public List<string> AlertToneFilePaths { get; set; } = new List<string>(); public List<string> AlertToneFilePaths { get; set; } = new List<string>();
/// <summary>
///
/// </summary>
public Dictionary<string, ChannelPosition> AlertTonePositions { get; set; } = new Dictionary<string, ChannelPosition>(); public Dictionary<string, ChannelPosition> AlertTonePositions { get; set; } = new Dictionary<string, ChannelPosition>();
/// <summary>
///
/// </summary>
public Dictionary<string, int> ChannelOutputDevices { get; set; } = new Dictionary<string, int>(); public Dictionary<string, int> ChannelOutputDevices { get; set; } = new Dictionary<string, int>();
/*
** Methods
*/
/// <summary>
///
/// </summary>
public void LoadSettings() public void LoadSettings()
{ {
if (!File.Exists(SettingsFilePath)) return; if (!File.Exists(SettingsFilePath)) return;
@ -60,6 +102,10 @@ namespace DVMConsole
} }
} }
/// <summary>
///
/// </summary>
/// <param name="newFilePath"></param>
public void UpdateAlertTonePaths(string newFilePath) public void UpdateAlertTonePaths(string newFilePath)
{ {
if (!AlertToneFilePaths.Contains(newFilePath)) if (!AlertToneFilePaths.Contains(newFilePath))
@ -69,30 +115,56 @@ namespace DVMConsole
} }
} }
/// <summary>
///
/// </summary>
/// <param name="alertFileName"></param>
/// <param name="x"></param>
/// <param name="y"></param>
public void UpdateAlertTonePosition(string alertFileName, double x, double y) public void UpdateAlertTonePosition(string alertFileName, double x, double y)
{ {
AlertTonePositions[alertFileName] = new ChannelPosition { X = x, Y = y }; AlertTonePositions[alertFileName] = new ChannelPosition { X = x, Y = y };
SaveSettings(); SaveSettings();
} }
/// <summary>
///
/// </summary>
/// <param name="channelName"></param>
/// <param name="x"></param>
/// <param name="y"></param>
public void UpdateChannelPosition(string channelName, double x, double y) public void UpdateChannelPosition(string channelName, double x, double y)
{ {
ChannelPositions[channelName] = new ChannelPosition { X = x, Y = y }; ChannelPositions[channelName] = new ChannelPosition { X = x, Y = y };
SaveSettings(); SaveSettings();
} }
/// <summary>
///
/// </summary>
/// <param name="systemName"></param>
/// <param name="x"></param>
/// <param name="y"></param>
public void UpdateSystemStatusPosition(string systemName, double x, double y) public void UpdateSystemStatusPosition(string systemName, double x, double y)
{ {
SystemStatusPositions[systemName] = new ChannelPosition { X = x, Y = y }; SystemStatusPositions[systemName] = new ChannelPosition { X = x, Y = y };
SaveSettings(); SaveSettings();
} }
/// <summary>
///
/// </summary>
/// <param name="channelName"></param>
/// <param name="deviceIndex"></param>
public void UpdateChannelOutputDevice(string channelName, int deviceIndex) public void UpdateChannelOutputDevice(string channelName, int deviceIndex)
{ {
ChannelOutputDevices[channelName] = deviceIndex; ChannelOutputDevices[channelName] = deviceIndex;
SaveSettings(); SaveSettings();
} }
/// <summary>
///
/// </summary>
public void SaveSettings() public void SaveSettings()
{ {
try try
@ -105,5 +177,5 @@ namespace DVMConsole
Console.WriteLine($"Error saving settings: {ex.Message}"); Console.WriteLine($"Error saving settings: {ex.Message}");
} }
} }
} } // public class SettingsManager
} } // namespace dvmconsole

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2024-2025 Caleb, K4PHP * Copyright (C) 2024-2025 Caleb, K4PHP
@ -13,27 +13,31 @@
using NAudio.Wave; using NAudio.Wave;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public class ToneGenerator public class ToneGenerator
{ {
private readonly int _sampleRate = 8000; private readonly int sampleRate = 8000;
private readonly int _bitsPerSample = 16; private readonly int bitsPerSample = 16;
private readonly int _channels = 1; private readonly int channels = 1;
private WaveOutEvent _waveOut; private WaveOutEvent waveOut;
private BufferedWaveProvider _waveProvider; private BufferedWaveProvider waveProvider;
/*
** Methods
*/
/// <summary> /// <summary>
/// Creates an instance of <see cref="ToneGenerator"/> /// Initializes a new instance of the <see cref="ToneGenerator"/> class.
/// </summary> /// </summary>
public ToneGenerator() public ToneGenerator()
{ {
_waveOut = new WaveOutEvent(); waveOut = new WaveOutEvent();
_waveProvider = new BufferedWaveProvider(new WaveFormat(_sampleRate, _bitsPerSample, _channels)); waveProvider = new BufferedWaveProvider(new WaveFormat(sampleRate, bitsPerSample, channels));
_waveOut.Init(_waveProvider); waveOut.Init(waveProvider);
} }
/// <summary> /// <summary>
@ -44,12 +48,12 @@ namespace DVMConsole
/// <returns>PCM data as a byte array</returns> /// <returns>PCM data as a byte array</returns>
public byte[] GenerateTone(double frequency, double durationSeconds) public byte[] GenerateTone(double frequency, double durationSeconds)
{ {
int sampleCount = (int)(_sampleRate * durationSeconds); int sampleCount = (int)(sampleRate * durationSeconds);
byte[] buffer = new byte[sampleCount * (_bitsPerSample / 8)]; byte[] buffer = new byte[sampleCount * (bitsPerSample / 8)];
for (int i = 0; i < sampleCount; i++) 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); short sampleValue = (short)(Math.Sin(2 * Math.PI * frequency * time) * short.MaxValue);
buffer[i * 2] = (byte)(sampleValue & 0xFF); buffer[i * 2] = (byte)(sampleValue & 0xFF);
@ -68,10 +72,10 @@ namespace DVMConsole
{ {
byte[] toneData = GenerateTone(frequency, durationSeconds); byte[] toneData = GenerateTone(frequency, durationSeconds);
_waveProvider.ClearBuffer(); waveProvider.ClearBuffer();
_waveProvider.AddSamples(toneData, 0, toneData.Length); waveProvider.AddSamples(toneData, 0, toneData.Length);
_waveOut.Play(); waveOut.Play();
} }
/// <summary> /// <summary>
@ -79,7 +83,7 @@ namespace DVMConsole
/// </summary> /// </summary>
public void StopTone() public void StopTone()
{ {
_waveOut.Stop(); waveOut.Stop();
} }
/// <summary> /// <summary>
@ -87,7 +91,7 @@ namespace DVMConsole
/// </summary> /// </summary>
public void Dispose() public void Dispose()
{ {
_waveOut.Dispose(); waveOut.Dispose();
} }
} } // public class ToneGenerator
} } // namespace dvmconsole

@ -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.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using fnecore;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary>
///
/// </summary>
public enum MBE_MODE public enum MBE_MODE
{ {
DMR_AMBE, //! DMR AMBE DMR_AMBE, //! DMR AMBE
IMBE_88BIT, //! 88-bit IMBE (P25) IMBE_88BIT, //! 88-bit IMBE (P25)
} } // public enum MBE_MODE
/// <summary> /// <summary>
/// Wrapper class for the c++ dvmvocoder encoder library /// Wrapper class for the C++ dvmvocoder encoder library.
/// </summary> /// </summary>
/// Using info from https://stackoverflow.com/a/315064/1842613 /// Using info from https://stackoverflow.com/a/315064/1842613
public class MBEEncoder public class MBEEncoder
{ {
private IntPtr encoder;
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="MBEEncoder"/> class.
/// </summary>
/// <param name="mode">Vocoder Mode (DMR or P25)</param>
public MBEEncoder(MBE_MODE mode)
{
encoder = MBEEncoder_Create(mode);
}
/// <summary>
/// Finalizes a instance of the <see cref="MBEEncoder"/> class.
/// </summary>
~MBEEncoder()
{
MBEEncoder_Delete(encoder);
}
/// <summary> /// <summary>
/// Create a new MBEEncoder /// Create a new MBEEncoder
/// </summary> /// </summary>
@ -54,28 +84,6 @@ namespace DVMConsole
[DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)] [DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)]
public static extern void MBEEncoder_Delete(IntPtr pEncoder); public static extern void MBEEncoder_Delete(IntPtr pEncoder);
/// <summary>
/// Pointer to the encoder instance
/// </summary>
private IntPtr encoder { get; set; }
/// <summary>
/// Create a new MBEEncoder instance
/// </summary>
/// <param name="mode">Vocoder Mode (DMR or P25)</param>
public MBEEncoder(MBE_MODE mode)
{
encoder = MBEEncoder_Create(mode);
}
/// <summary>
/// Private class destructor properly deletes interop instance
/// </summary>
~MBEEncoder()
{
MBEEncoder_Delete(encoder);
}
/// <summary> /// <summary>
/// Encode PCM16 samples to MBE codeword /// Encode PCM16 samples to MBE codeword
/// </summary> /// </summary>
@ -86,17 +94,45 @@ namespace DVMConsole
MBEEncoder_Encode(encoder, samples, codeword); MBEEncoder_Encode(encoder, samples, codeword);
} }
/// <summary>
///
/// </summary>
/// <param name="bits"></param>
/// <param name="codeword"></param>
public void encodeBits([In] char[] bits, [Out] byte[] codeword) public void encodeBits([In] char[] bits, [Out] byte[] codeword)
{ {
MBEEncoder_EncodeBits(encoder, bits, codeword); MBEEncoder_EncodeBits(encoder, bits, codeword);
} }
} } // public class MBEEncoder
/// <summary> /// <summary>
/// Wrapper class for the c++ dvmvocoder decoder library /// Wrapper class for the C++ dvmvocoder decoder library.
/// </summary> /// </summary>
public class MBEDecoder public class MBEDecoder
{ {
private IntPtr decoder;
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="MBEDecoder"/> class.
/// </summary>
/// <param name="mode">Vocoder Mode (DMR or P25)</param>
public MBEDecoder(MBE_MODE mode)
{
decoder = MBEDecoder_Create(mode);
}
/// <summary>
/// Finalizes a instance of the <see cref="MBEDecoder"/> class.
/// </summary>
~MBEDecoder()
{
MBEDecoder_Delete(decoder);
}
/// <summary> /// <summary>
/// Create a new MBEDecoder /// Create a new MBEDecoder
/// </summary> /// </summary>
@ -130,28 +166,6 @@ namespace DVMConsole
[DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)] [DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)]
public static extern void MBEDecoder_Delete(IntPtr pDecoder); public static extern void MBEDecoder_Delete(IntPtr pDecoder);
/// <summary>
/// Pointer to the decoder instance
/// </summary>
private IntPtr decoder { get; set; }
/// <summary>
/// Create a new MBEDecoder instance
/// </summary>
/// <param name="mode">Vocoder Mode (DMR or P25)</param>
public MBEDecoder(MBE_MODE mode)
{
decoder = MBEDecoder_Create(mode);
}
/// <summary>
/// Private class destructor properly deletes interop instance
/// </summary>
~MBEDecoder()
{
MBEDecoder_Delete(decoder);
}
/// <summary> /// <summary>
/// Decode MBE codeword to PCM16 samples /// Decode MBE codeword to PCM16 samples
/// </summary> /// </summary>
@ -172,8 +186,11 @@ namespace DVMConsole
{ {
return MBEDecoder_DecodeBits(decoder, codeword, bits); return MBEDecoder_DecodeBits(decoder, codeword, bits);
} }
} } // public class MBEDecoder
/// <summary>
///
/// </summary>
public static class MBEToneGenerator public static class MBEToneGenerator
{ {
/// <summary> /// <summary>
@ -183,7 +200,7 @@ namespace DVMConsole
/// <param name="tone_amplitude"></param> /// <param name="tone_amplitude"></param>
/// <param name="codeword"></param> /// <param name="codeword"></param>
/// <exception cref="ArgumentOutOfRangeException"></exception> /// <exception cref="ArgumentOutOfRangeException"></exception>
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 // U bit vectors
// u0 and u1 are 12 bits // u0 and u1 are 12 bits
@ -197,15 +214,11 @@ namespace DVMConsole
// Validate tone index // Validate tone index
if (tone_idx < 5 || tone_idx > 122) if (tone_idx < 5 || tone_idx > 122)
{
throw new ArgumentOutOfRangeException($"Tone index for frequency out of range!"); throw new ArgumentOutOfRangeException($"Tone index for frequency out of range!");
}
// Validate amplitude value // Validate amplitude value
if (tone_amplitude > 127) if (tone_amplitude > 127)
{
throw new ArgumentOutOfRangeException("Tone amplitude must be between 0 and 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 :) ) // Make sure tone index only has 7 bits (it should but we make sure :) )
tone_idx &= 0b01111111; tone_idx &= 0b01111111;
@ -258,8 +271,11 @@ namespace DVMConsole
byte[] tone_codeword = VocoderToneLookupTable.IMBEToneFrames[nearest]; byte[] tone_codeword = VocoderToneLookupTable.IMBEToneFrames[nearest];
Array.Copy(tone_codeword, codeword, tone_codeword.Length); Array.Copy(tone_codeword, codeword, tone_codeword.Length);
} }
} } // public static class MBEToneGenerator
/// <summary>
///
/// </summary>
public class MBEInterleaver public class MBEInterleaver
{ {
public const int PCM_SAMPLES = 160; public const int PCM_SAMPLES = 160;
@ -273,6 +289,14 @@ namespace DVMConsole
private MBEEncoder encoder; private MBEEncoder encoder;
private MBEDecoder decoder; private MBEDecoder decoder;
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="MBEInterleaver"/> class.
/// </summary>
/// <param name="mode"></param>
public MBEInterleaver(MBE_MODE mode) public MBEInterleaver(MBE_MODE mode)
{ {
this.mode = mode; this.mode = mode;
@ -280,13 +304,19 @@ namespace DVMConsole
decoder = new MBEDecoder(this.mode); decoder = new MBEDecoder(this.mode);
} }
public Int32 Decode([In] byte[] codeword, [Out] byte[] mbeBits) /// <summary>
///
/// </summary>
/// <param name="codeword"></param>
/// <param name="mbeBits"></param>
/// <returns></returns>
/// <exception cref="NullReferenceException"></exception>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public int Decode([In] byte[] codeword, [Out] byte[] mbeBits)
{ {
// Input validation // Input validation
if (codeword == null) if (codeword == null)
{
throw new NullReferenceException("Input MBE codeword is null!"); throw new NullReferenceException("Input MBE codeword is null!");
}
char[] bits = null; char[] bits = null;
@ -294,24 +324,18 @@ namespace DVMConsole
if (mode == MBE_MODE.DMR_AMBE) if (mode == MBE_MODE.DMR_AMBE)
{ {
if (codeword.Length != AMBE_CODEWORD_SAMPLES) if (codeword.Length != AMBE_CODEWORD_SAMPLES)
{
throw new ArgumentOutOfRangeException($"AMBE codeword length is != {AMBE_CODEWORD_SAMPLES}"); throw new ArgumentOutOfRangeException($"AMBE codeword length is != {AMBE_CODEWORD_SAMPLES}");
}
bits = new char[AMBE_CODEWORD_BITS]; bits = new char[AMBE_CODEWORD_BITS];
} }
else if (mode == MBE_MODE.IMBE_88BIT) else if (mode == MBE_MODE.IMBE_88BIT)
{ {
if (codeword.Length != IMBE_CODEWORD_SAMPLES) if (codeword.Length != IMBE_CODEWORD_SAMPLES)
{
throw new ArgumentOutOfRangeException($"IMBE codeword length is != {IMBE_CODEWORD_SAMPLES}"); throw new ArgumentOutOfRangeException($"IMBE codeword length is != {IMBE_CODEWORD_SAMPLES}");
}
bits = new char[IMBE_CODEWORD_BITS]; bits = new char[IMBE_CODEWORD_BITS];
} }
if (bits == null) if (bits == null)
{
throw new NullReferenceException("Failed to initialize decoder"); throw new NullReferenceException("Failed to initialize decoder");
}
// Decode // Decode
int errs = decoder.decodeBits(codeword, bits); int errs = decoder.decodeBits(codeword, bits);
@ -334,12 +358,18 @@ namespace DVMConsole
return errs; return errs;
} }
/// <summary>
///
/// </summary>
/// <param name="mbeBits"></param>
/// <param name="codeword"></param>
/// <exception cref="NullReferenceException"></exception>
/// <exception cref="ArgumentOutOfRangeException"></exception>
/// <exception cref="ArgumentException"></exception>
public void Encode([In] byte[] mbeBits, [Out] byte[] codeword) public void Encode([In] byte[] mbeBits, [Out] byte[] codeword)
{ {
if (mbeBits == null) if (mbeBits == null)
{
throw new NullReferenceException("Input MBE bit array is null!"); throw new NullReferenceException("Input MBE bit array is null!");
}
char[] bits = null; char[] bits = null;
@ -347,34 +377,30 @@ namespace DVMConsole
if (mode == MBE_MODE.DMR_AMBE) if (mode == MBE_MODE.DMR_AMBE)
{ {
if (mbeBits.Length != AMBE_CODEWORD_BITS) if (mbeBits.Length != AMBE_CODEWORD_BITS)
{
throw new ArgumentOutOfRangeException($"AMBE codeword bit length is != {AMBE_CODEWORD_BITS}"); throw new ArgumentOutOfRangeException($"AMBE codeword bit length is != {AMBE_CODEWORD_BITS}");
}
bits = new char[AMBE_CODEWORD_BITS]; bits = new char[AMBE_CODEWORD_BITS];
Array.Copy(mbeBits, bits, AMBE_CODEWORD_BITS); Array.Copy(mbeBits, bits, AMBE_CODEWORD_BITS);
} }
else if (mode == MBE_MODE.IMBE_88BIT) else if (mode == MBE_MODE.IMBE_88BIT)
{ {
if (mbeBits.Length != IMBE_CODEWORD_BITS) if (mbeBits.Length != IMBE_CODEWORD_BITS)
{
throw new ArgumentOutOfRangeException($"IMBE codeword bit length is != {AMBE_CODEWORD_BITS}"); throw new ArgumentOutOfRangeException($"IMBE codeword bit length is != {AMBE_CODEWORD_BITS}");
}
bits = new char[IMBE_CODEWORD_BITS]; bits = new char[IMBE_CODEWORD_BITS];
Array.Copy(mbeBits, bits, IMBE_CODEWORD_BITS); Array.Copy(mbeBits, bits, IMBE_CODEWORD_BITS);
} }
if (bits == null) if (bits == null)
{
throw new ArgumentException("Bit array did not get set up properly!"); throw new ArgumentException("Bit array did not get set up properly!");
}
// Encode samples // Encode samples
if (mode == MBE_MODE.DMR_AMBE) if (mode == MBE_MODE.DMR_AMBE)
{ {
// Create output array // Create output array
byte[] codewords = new byte[AMBE_CODEWORD_SAMPLES]; byte[] codewords = new byte[AMBE_CODEWORD_SAMPLES];
// Encode // Encode
encoder.encodeBits(bits, codewords); encoder.encodeBits(bits, codewords);
// Copy // Copy
codeword = new byte[AMBE_CODEWORD_SAMPLES]; codeword = new byte[AMBE_CODEWORD_SAMPLES];
Array.Copy(codewords, codeword, IMBE_CODEWORD_SAMPLES); Array.Copy(codewords, codeword, IMBE_CODEWORD_SAMPLES);
@ -383,12 +409,14 @@ namespace DVMConsole
{ {
// Create output array // Create output array
byte[] codewords = new byte[IMBE_CODEWORD_SAMPLES]; byte[] codewords = new byte[IMBE_CODEWORD_SAMPLES];
// Encode // Encode
encoder.encodeBits(bits, codewords); encoder.encodeBits(bits, codewords);
// Copy // Copy
codeword = new byte[IMBE_CODEWORD_SAMPLES]; codeword = new byte[IMBE_CODEWORD_SAMPLES];
Array.Copy(codewords, codeword, IMBE_CODEWORD_SAMPLES); Array.Copy(codewords, codeword, IMBE_CODEWORD_SAMPLES);
} }
} }
} } // public class MBEInterleaver
} } // namespace dvmconsole

@ -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
{ {
/// <summary> /// <summary>
/// From https://github.com/W3AXL/rc2-dvm/blob/main/rc2-dvm/Audio.cs /// 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 } }, { 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 } }, { 2500, new byte[] { 0x5, 0x6, 0xFB, 0x63, 0xCD, 0xD9, 0x2B, 0x42, 0xE1, 0xCF, 0x6B } },
}; };
} } // public class VocoderToneLookupTable
} } // namespace dvmconsole

@ -1,91 +1,125 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2024 Caleb, K4PHP * Copyright (C) 2024 Caleb, K4PHP
* *
*/ */
using NAudio.Wave;
using System.Windows.Threading; using System.Windows.Threading;
namespace DVMConsole using NAudio.Wave;
namespace dvmconsole
{ {
/// <summary>
///
/// </summary>
public class WaveFilePlaybackManager public class WaveFilePlaybackManager
{ {
private readonly string _waveFilePath; private readonly string waveFilePath;
private readonly DispatcherTimer _timer; private readonly DispatcherTimer timer;
private WaveOutEvent _waveOut; private WaveOutEvent waveOut;
private AudioFileReader _audioFileReader; private AudioFileReader audioFileReader;
private bool _isPlaying; private bool isPlaying;
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="WaveFilePlaybackManager"/> class.
/// </summary>
/// <param name="waveFilePath"></param>
/// <param name="intervalMilliseconds"></param>
/// <exception cref="ArgumentNullException"></exception>
public WaveFilePlaybackManager(string waveFilePath, int intervalMilliseconds = 500) public WaveFilePlaybackManager(string waveFilePath, int intervalMilliseconds = 500)
{ {
if (string.IsNullOrEmpty(waveFilePath)) if (string.IsNullOrEmpty(waveFilePath))
throw new ArgumentNullException(nameof(waveFilePath), "Wave file path cannot be null or empty."); throw new ArgumentNullException(nameof(waveFilePath), "Wave file path cannot be null or empty.");
_waveFilePath = waveFilePath; this.waveFilePath = waveFilePath;
_timer = new DispatcherTimer timer = new DispatcherTimer
{ {
Interval = TimeSpan.FromMilliseconds(intervalMilliseconds) Interval = TimeSpan.FromMilliseconds(intervalMilliseconds)
}; };
_timer.Tick += OnTimerTick; timer.Tick += OnTimerTick;
} }
/// <summary>
///
/// </summary>
public void Start() public void Start()
{ {
if (_isPlaying) if (isPlaying)
return; return;
InitializeAudio(); InitializeAudio();
_isPlaying = true; isPlaying = true;
_timer.Start(); timer.Start();
} }
/// <summary>
///
/// </summary>
public void Stop() public void Stop()
{ {
if (!_isPlaying) if (!isPlaying)
return; return;
_timer.Stop(); timer.Stop();
DisposeAudio(); DisposeAudio();
_isPlaying = false; isPlaying = false;
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnTimerTick(object sender, EventArgs e) private void OnTimerTick(object sender, EventArgs e)
{ {
PlayAudio(); PlayAudio();
} }
/// <summary>
///
/// </summary>
private void InitializeAudio() private void InitializeAudio()
{ {
_audioFileReader = new AudioFileReader(_waveFilePath); audioFileReader = new AudioFileReader(waveFilePath);
_waveOut = new WaveOutEvent(); waveOut = new WaveOutEvent();
_waveOut.Init(_audioFileReader); waveOut.Init(audioFileReader);
} }
/// <summary>
///
/// </summary>
private void PlayAudio() private void PlayAudio()
{ {
if (_waveOut != null && _waveOut.PlaybackState != PlaybackState.Playing) if (waveOut != null && waveOut.PlaybackState != PlaybackState.Playing)
{ {
_waveOut.Stop(); waveOut.Stop();
_audioFileReader.Position = 0; audioFileReader.Position = 0;
_waveOut.Play(); waveOut.Play();
} }
} }
/// <summary>
///
/// </summary>
private void DisposeAudio() private void DisposeAudio()
{ {
_waveOut?.Stop(); waveOut?.Stop();
_waveOut?.Dispose(); waveOut?.Dispose();
_audioFileReader?.Dispose(); audioFileReader?.Dispose();
_waveOut = null; waveOut = null;
_audioFileReader = null; audioFileReader = null;
} }
} } // public class WaveFilePlaybackManager
} } // namespace dvmconsole

@ -1,4 +1,4 @@
<Window x:Class="DVMConsole.WidgetSelectionWindow" <Window x:Class="dvmconsole.WidgetSelectionWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Select Widgets" Height="200" Width="300" WindowStartupLocation="CenterOwner"> Title="Select Widgets" Height="200" Width="300" WindowStartupLocation="CenterOwner">

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2024 Caleb, K4PHP * Copyright (C) 2024 Caleb, K4PHP
@ -13,19 +13,47 @@
using System.Windows; using System.Windows;
namespace DVMConsole namespace dvmconsole
{ {
/// <summary>
/// Interaction logic for WidgetSelectionWindow.xaml
/// </summary>
public partial class WidgetSelectionWindow : Window public partial class WidgetSelectionWindow : Window
{ {
/*
** Properties
*/
/// <summary>
///
/// </summary>
public bool ShowSystemStatus { get; private set; } = true; public bool ShowSystemStatus { get; private set; } = true;
/// <summary>
///
/// </summary>
public bool ShowChannels { get; private set; } = true; public bool ShowChannels { get; private set; } = true;
/// <summary>
///
/// </summary>
public bool ShowAlertTones { get; private set; } = true; public bool ShowAlertTones { get; private set; } = true;
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="WidgetSelectionWindow"/> class.
/// </summary>
public WidgetSelectionWindow() public WidgetSelectionWindow()
{ {
InitializeComponent(); InitializeComponent();
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ApplyButton_Click(object sender, RoutedEventArgs e) private void ApplyButton_Click(object sender, RoutedEventArgs e)
{ {
ShowSystemStatus = SystemStatusCheckBox.IsChecked ?? false; ShowSystemStatus = SystemStatusCheckBox.IsChecked ?? false;
@ -34,5 +62,5 @@ namespace DVMConsole
DialogResult = true; DialogResult = true;
Close(); Close();
} }
} } // public partial class WidgetSelectionWindow : Window
} } // namespace dvmconsole

@ -33,8 +33,8 @@ zones:
tgid: "2001" tgid: "2001"
# Encryption Key Id (If 0 or blank, will be assumed clear) # Encryption Key Id (If 0 or blank, will be assumed clear)
keyId: 0x50 keyId: 0x50
# Algorithm Id 0xAA or 0x84 (RC4 or AES) (If 0 or blank, will be assumed clear) # Algorithm AES ("aes"), ADP/ARC4 ("arc4"), None ("none")
algoId: 0xaa algo: "aes"
# Ignored now, we use dvmfne KMM support (This will be used in the future to ovveride FNE KMM support) # Ignored now, we use dvmfne KMM support (This will be used in the future to ovveride FNE KMM support)
encryptionKey: null encryptionKey: null
- name: "Channel 2" - name: "Channel 2"

@ -1,12 +1,18 @@
# DVMConsole # Digital Voice Modem Desktop Dispatch Console
### DVM Desktop Console
![console](./images/consolehome.JPG) This provides a desktop application that mimics or otherwise operates like a system dispatch console.
## Setup ## Setup
- Download the packaged release from the releases or clone and build yourself - Download the packaged release from the releases or clone and build yourself
- Modify the codeplug file - Modify the codeplug file
- Select the codeplug once opening the app - Select the codeplug once opening the app
## Features ## Features
- Custumizable widgets - Custumizable widgets
- Individual channel audio control - Individual channel audio control
- AES and RC4 crypto support - AES and RC4 crypto support
- Auto saved and transferable user settings - 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.

@ -1,4 +1,4 @@
<UserControl x:Class="DVMConsole.Controls.AlertTone" <UserControl x:Class="dvmconsole.Controls.AlertTone"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="55" Width="105" Height="55" Width="105"
@ -26,9 +26,6 @@
</Button.Background> </Button.Background>
<Image Height="40" Width="63" Source="/Assets/page.png" Margin="0,-8,0,0"/> <Image Height="40" Width="63" Source="/Assets/page.png" Margin="0,-8,0,0"/>
</Button> </Button>
<TextBox HorizontalAlignment="Center" FontWeight="Bold" FontSize="10" TextWrapping="Wrap" Text="{Binding AlertFileName}" TextAlignment="Center" VerticalAlignment="Top" Width="100" Background="{x:Null}" BorderThickness="0,0,0,0" Height="24" Margin="0,4,0,0" Grid.RowSpan="2" TextChanged="TextBox_TextChanged" Foreground="White"/> <TextBox HorizontalAlignment="Center" FontWeight="Bold" FontSize="10" TextWrapping="Wrap" Text="{Binding AlertFileName}" TextAlignment="Center" VerticalAlignment="Top" Width="100" Background="{x:Null}" BorderThickness="0,0,0,0" Height="24" Margin="0,4,0,0" Grid.RowSpan="2" Foreground="White"/>
</Grid> </Grid>
</UserControl> </UserControl>

@ -1,51 +1,71 @@
/* // SPDX-License-Identifier: AGPL-3.0-only
* WhackerLink - 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.
* *
* This program is free software: you can redistribute it and/or modify * @package DVM / Desktop Dispatch Console
* it under the terms of the GNU General Public License as published by * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * Copyright (C) 2025 Caleb, K4PHP
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright (C) 2024 Caleb, K4PHP
*
*/ */
using System.Media;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
namespace DVMConsole.Controls namespace dvmconsole.Controls
{ {
/// <summary>
///
/// </summary>
public partial class AlertTone : UserControl public partial class AlertTone : UserControl
{ {
public event Action<AlertTone> OnAlertTone; private Point startPoint;
private bool isDragging;
public static readonly DependencyProperty AlertFileNameProperty = public static readonly DependencyProperty AlertFileNameProperty =
DependencyProperty.Register("AlertFileName", typeof(string), typeof(AlertTone), new PropertyMetadata(string.Empty)); DependencyProperty.Register("AlertFileName", typeof(string), typeof(AlertTone), new PropertyMetadata(string.Empty));
/*
** Properties
*/
/// <summary>
///
/// </summary>
public string AlertFileName public string AlertFileName
{ {
get => (string)GetValue(AlertFileNameProperty); get => (string)GetValue(AlertFileNameProperty);
set => SetValue(AlertFileNameProperty, value); set => SetValue(AlertFileNameProperty, value);
} }
/// <summary>
///
/// </summary>
public string AlertFilePath { get; set; } public string AlertFilePath { get; set; }
private Point _startPoint; /// <summary>
private bool _isDragging; ///
/// </summary>
public bool IsEditMode { get; set; } public bool IsEditMode { get; set; }
/*
** Events
*/
public event Action<AlertTone> OnAlertTone;
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="AlertTone"/> class.
/// </summary>
/// <param name="alertFilePath"></param>
public AlertTone(string alertFilePath) public AlertTone(string alertFilePath)
{ {
InitializeComponent(); InitializeComponent();
@ -57,29 +77,44 @@ namespace DVMConsole.Controls
this.MouseRightButtonDown += AlertTone_MouseRightButtonDown; this.MouseRightButtonDown += AlertTone_MouseRightButtonDown;
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void PlayAlert_Click(object sender, RoutedEventArgs e) private void PlayAlert_Click(object sender, RoutedEventArgs e)
{ {
OnAlertTone.Invoke(this); OnAlertTone.Invoke(this);
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void AlertTone_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) private void AlertTone_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{ {
if (!IsEditMode) return; if (!IsEditMode) return;
_startPoint = e.GetPosition(this); startPoint = e.GetPosition(this);
_isDragging = true; isDragging = true;
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void AlertTone_MouseMove(object sender, MouseEventArgs e) private void AlertTone_MouseMove(object sender, MouseEventArgs e)
{ {
if (_isDragging && IsEditMode) if (isDragging && IsEditMode)
{ {
var parentCanvas = VisualTreeHelper.GetParent(this) as Canvas; var parentCanvas = VisualTreeHelper.GetParent(this) as Canvas;
if (parentCanvas != null) if (parentCanvas != null)
{ {
Point mousePos = e.GetPosition(parentCanvas); Point mousePos = e.GetPosition(parentCanvas);
double newLeft = mousePos.X - _startPoint.X; double newLeft = mousePos.X - startPoint.X;
double newTop = mousePos.Y - _startPoint.Y; double newTop = mousePos.Y - startPoint.Y;
Canvas.SetLeft(this, Math.Max(0, newLeft)); Canvas.SetLeft(this, Math.Max(0, newLeft));
Canvas.SetTop(this, Math.Max(0, newTop)); Canvas.SetTop(this, Math.Max(0, newTop));
@ -87,11 +122,16 @@ namespace DVMConsole.Controls
} }
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void AlertTone_MouseRightButtonDown(object sender, MouseButtonEventArgs e) 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; var parentCanvas = VisualTreeHelper.GetParent(this) as Canvas;
if (parentCanvas != null) if (parentCanvas != null)
@ -102,10 +142,5 @@ namespace DVMConsole.Controls
ReleaseMouseCapture(); ReleaseMouseCapture();
} }
} // public partial class AlertTone : UserControl
private void TextBox_TextChanged(object sender, TextChangedEventArgs e) } // namespace dvmconsole.Controls
{
}
}
}

@ -1,4 +1,4 @@
<UserControl x:Class="DVMConsole.Controls.ChannelBox" <UserControl x:Class="dvmconsole.Controls.ChannelBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="220" Height="100" Background="#FF0B004B"> Width="220" Height="100" Background="#FF0B004B">
@ -42,9 +42,9 @@
<!-- Volume Slider --> <!-- Volume Slider -->
<Slider Minimum="0" Maximum="4" Value="{Binding Volume, Mode=TwoWay}" <Slider Minimum="0" Maximum="4" Value="{Binding Volume, Mode=TwoWay}"
Height="21" VerticalAlignment="Top" x:Name="VolumeSlider" Height="21" VerticalAlignment="Top" x:Name="VolumeSlider"
ValueChanged="VolumeSlider_ValueChanged" Margin="11,10,65,0" ValueChanged="VolumeSlider_ValueChanged" Margin="11,10,65,0"
Grid.ColumnSpan="2" Grid.Row="2"> Grid.ColumnSpan="2" Grid.Row="2">
<Slider.Style> <Slider.Style>
<Style TargetType="Slider"> <Style TargetType="Slider">
<Setter Property="Template"> <Setter Property="Template">
@ -77,8 +77,6 @@
</Slider.Style> </Slider.Style>
</Slider> </Slider>
<!-- Bottom Buttons --> <!-- Bottom Buttons -->
<StackPanel Grid.Column="1" Grid.Row="2" Orientation="Horizontal" Margin="81,0,-2,0" Grid.RowSpan="2" Grid.ColumnSpan="2"> <StackPanel Grid.Column="1" Grid.Row="2" Orientation="Horizontal" Margin="81,0,-2,0" Grid.RowSpan="2" Grid.ColumnSpan="2">
<Button Width="40" Height="40" x:Name="PageSelectButton" Click="PageSelectButton_Click" BorderThickness="0,0,0,0"> <Button Width="40" Height="40" x:Name="PageSelectButton" Click="PageSelectButton_Click" BorderThickness="0,0,0,0">

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2025 Caleb, K4PHP * Copyright (C) 2025 Caleb, K4PHP
@ -12,38 +12,36 @@
*/ */
using System.ComponentModel; using System.ComponentModel;
using System.Security.Cryptography;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using fnecore.P25; using fnecore.P25;
namespace DVMConsole.Controls namespace dvmconsole.Controls
{ {
/// <summary>
///
/// </summary>
public partial class ChannelBox : UserControl, INotifyPropertyChanged public partial class ChannelBox : UserControl, INotifyPropertyChanged
{ {
private readonly SelectedChannelsManager _selectedChannelsManager; private readonly SelectedChannelsManager selectedChannelsManager;
private readonly AudioManager _audioManager; private readonly AudioManager audioManager;
private bool _pttState; private bool pttState;
private bool _pageState; private bool pageState;
private bool _holdState; private bool holdState;
private bool _emergency; private bool emergency;
private string _lastSrcId = "0"; private string lastSrcId = "0";
private double _volume = 1.0; private double volume = 1.0;
private bool isSelected;
internal LinearGradientBrush grayGradient; internal LinearGradientBrush grayGradient;
internal LinearGradientBrush redGradient; internal LinearGradientBrush redGradient;
internal LinearGradientBrush orangeGradient; internal LinearGradientBrush orangeGradient;
public FlashingBackgroundManager _flashingBackgroundManager; public FlashingBackgroundManager flashingBackgroundManager;
public event EventHandler<ChannelBox> PTTButtonClicked;
public event EventHandler<ChannelBox> PageButtonClicked;
public event EventHandler<ChannelBox> HoldChannelButtonClicked;
public event PropertyChangedEventHandler PropertyChanged;
public byte[] netLDU1 = new byte[9 * 25]; public byte[] netLDU1 = new byte[9 * 25];
public byte[] netLDU2 = new byte[9 * 25]; public byte[] netLDU2 = new byte[9 * 25];
@ -58,10 +56,6 @@ namespace DVMConsole.Controls
public List<byte[]> chunkedPcm = new List<byte[]>(); public List<byte[]> chunkedPcm = new List<byte[]>();
public string ChannelName { get; set; }
public string SystemName { get; set; }
public string DstId { get; set; }
#if WIN32 #if WIN32
public AmbeVocoder extFullRateVocoder; public AmbeVocoder extFullRateVocoder;
public AmbeVocoder extHalfRateVocoder; public AmbeVocoder extHalfRateVocoder;
@ -73,111 +67,196 @@ namespace DVMConsole.Controls
public P25Crypto crypter = new P25Crypto(); public P25Crypto crypter = new P25Crypto();
/*
** Properties
*/
/// <summary>
///
/// </summary>
public string ChannelName { get; set; }
/// <summary>
///
/// </summary>
public string SystemName { get; set; }
/// <summary>
///
/// </summary>
public string DstId { get; set; }
/*
** Events
*/
/// <summary>
///
/// </summary>
public event EventHandler<ChannelBox> PTTButtonClicked;
/// <summary>
///
/// </summary>
public event EventHandler<ChannelBox> PageButtonClicked;
/// <summary>
///
/// </summary>
public event EventHandler<ChannelBox> HoldChannelButtonClicked;
/// <summary>
///
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
///
/// </summary>
public bool IsReceiving { get; set; } = false; public bool IsReceiving { get; set; } = false;
/// <summary>
///
/// </summary>
public bool IsReceivingEncrypted { get; set; } = false; public bool IsReceivingEncrypted { get; set; } = false;
/// <summary>
///
/// </summary>
public string LastSrcId public string LastSrcId
{ {
get => _lastSrcId; get => lastSrcId;
set set
{ {
if (_lastSrcId != value) if (lastSrcId != value)
{ {
_lastSrcId = value; lastSrcId = value;
OnPropertyChanged(nameof(LastSrcId)); OnPropertyChanged(nameof(LastSrcId));
} }
} }
} }
/// <summary>
///
/// </summary>
public bool PttState public bool PttState
{ {
get => _pttState; get => pttState;
set set
{ {
_pttState = value; pttState = value;
UpdatePTTColor(); UpdatePTTColor();
} }
} }
/// <summary>
///
/// </summary>
public bool PageState public bool PageState
{ {
get => _pageState; get => pageState;
set set
{ {
_pageState = value; pageState = value;
UpdatePageColor(); UpdatePageColor();
} }
} }
/// <summary>
///
/// </summary>
public bool HoldState public bool HoldState
{ {
get => _holdState; get => holdState;
set set
{ {
_holdState = value; holdState = value;
UpdateHoldColor(); UpdateHoldColor();
} }
} }
/// <summary>
///
/// </summary>
public bool Emergency public bool Emergency
{ {
get => _emergency; get => emergency;
set set
{ {
_emergency = value; emergency = value;
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
{ {
if (value) if (value)
_flashingBackgroundManager.Start(); flashingBackgroundManager.Start();
else else
_flashingBackgroundManager.Stop(); flashingBackgroundManager.Stop();
}); });
} }
} }
/// <summary>
///
/// </summary>
public string VoiceChannel { get; set; } public string VoiceChannel { get; set; }
/// <summary>
///
/// </summary>
public bool IsEditMode { get; set; } public bool IsEditMode { get; set; }
private bool _isSelected; /// <summary>
///
/// </summary>
public bool IsSelected public bool IsSelected
{ {
get => _isSelected; get => isSelected;
set set
{ {
_isSelected = value; isSelected = value;
UpdateBackground(); UpdateBackground();
} }
} }
/// <summary>
///
/// </summary>
public double Volume public double Volume
{ {
get => _volume; get => volume;
set set
{ {
if (_volume != value) if (volume != value)
{ {
_volume = value; volume = value;
OnPropertyChanged(nameof(Volume)); OnPropertyChanged(nameof(Volume));
_audioManager.SetTalkgroupVolume(DstId, (float)value); audioManager.SetTalkgroupVolume(DstId, (float)value);
} }
} }
} }
/// <summary>
///
/// </summary>
public uint txStreamId { get; internal set; } public uint txStreamId { get; internal set; }
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="ChannelBox"/> class.
/// </summary>
/// <param name="selectedChannelsManager"></param>
/// <param name="audioManager"></param>
/// <param name="channelName"></param>
/// <param name="systemName"></param>
/// <param name="dstId"></param>
public ChannelBox(SelectedChannelsManager selectedChannelsManager, AudioManager audioManager, string channelName, string systemName, string dstId) public ChannelBox(SelectedChannelsManager selectedChannelsManager, AudioManager audioManager, string channelName, string systemName, string dstId)
{ {
InitializeComponent(); InitializeComponent();
DataContext = this; DataContext = this;
_selectedChannelsManager = selectedChannelsManager; this.selectedChannelsManager = selectedChannelsManager;
_audioManager = audioManager; this.audioManager = audioManager;
_flashingBackgroundManager = new FlashingBackgroundManager(this); flashingBackgroundManager = new FlashingBackgroundManager(this);
ChannelName = channelName; ChannelName = channelName;
DstId = dstId; DstId = dstId;
SystemName = $"System: {systemName}"; SystemName = $"System: {systemName}";
LastSrcId = $"Last SRC: {LastSrcId}"; LastSrcId = $"Last ID: {LastSrcId}";
UpdateBackground(); UpdateBackground();
MouseLeftButtonDown += ChannelBox_MouseLeftButtonDown; MouseLeftButtonDown += ChannelBox_MouseLeftButtonDown;
@ -220,26 +299,32 @@ namespace DVMConsole.Controls
} }
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ChannelBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) private void ChannelBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{ {
if (IsEditMode) return; if (IsEditMode)
return;
IsSelected = !IsSelected; IsSelected = !IsSelected;
Background = IsSelected ? (Brush)new BrushConverter().ConvertFrom("#FF0B004B") : Brushes.Gray; Background = IsSelected ? (Brush)new BrushConverter().ConvertFrom("#FF0B004B") : Brushes.Gray;
if (IsSelected) if (IsSelected)
{ selectedChannelsManager.AddSelectedChannel(this);
_selectedChannelsManager.AddSelectedChannel(this);
}
else else
{ selectedChannelsManager.RemoveSelectedChannel(this);
_selectedChannelsManager.RemoveSelectedChannel(this);
}
} }
/// <summary>
///
/// </summary>
private void UpdatePTTColor() private void UpdatePTTColor()
{ {
if (IsEditMode) return; if (IsEditMode)
return;
if (PttState) if (PttState)
PttButton.Background = redGradient; PttButton.Background = redGradient;
@ -247,9 +332,13 @@ namespace DVMConsole.Controls
PttButton.Background = grayGradient; PttButton.Background = grayGradient;
} }
/// <summary>
///
/// </summary>
private void UpdatePageColor() private void UpdatePageColor()
{ {
if (IsEditMode) return; if (IsEditMode)
return;
if (PageState) if (PageState)
PageSelectButton.Background = orangeGradient; PageSelectButton.Background = orangeGradient;
@ -259,7 +348,8 @@ namespace DVMConsole.Controls
private void UpdateHoldColor() private void UpdateHoldColor()
{ {
if (IsEditMode) return; if (IsEditMode)
return;
if (HoldState) if (HoldState)
ChannelMarkerBtn.Background = orangeGradient; ChannelMarkerBtn.Background = orangeGradient;
@ -267,6 +357,9 @@ namespace DVMConsole.Controls
ChannelMarkerBtn.Background = grayGradient; ChannelMarkerBtn.Background = grayGradient;
} }
/// <summary>
///
/// </summary>
private void UpdateBackground() private void UpdateBackground()
{ {
if (SystemName == MainWindow.PLAYBACKSYS || ChannelName == MainWindow.PLAYBACKCHNAME || DstId == MainWindow.PLAYBACKTG) if (SystemName == MainWindow.PLAYBACKSYS || ChannelName == MainWindow.PLAYBACKCHNAME || DstId == MainWindow.PLAYBACKTG)
@ -278,9 +371,15 @@ namespace DVMConsole.Controls
Background = IsSelected ? (Brush)new BrushConverter().ConvertFrom("#FF0B004B") : Brushes.DarkGray; Background = IsSelected ? (Brush)new BrushConverter().ConvertFrom("#FF0B004B") : Brushes.DarkGray;
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void PTTButton_Click(object sender, RoutedEventArgs e) private async void PTTButton_Click(object sender, RoutedEventArgs e)
{ {
if (!IsSelected) return; if (!IsSelected)
return;
if (PttState) if (PttState)
await Task.Delay(500); await Task.Delay(500);
@ -290,44 +389,77 @@ namespace DVMConsole.Controls
PTTButtonClicked.Invoke(sender, this); PTTButtonClicked.Invoke(sender, this);
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void PageSelectButton_Click(object sender, RoutedEventArgs e) private void PageSelectButton_Click(object sender, RoutedEventArgs e)
{ {
if (!IsSelected) return; if (!IsSelected)
return;
PageState = !PageState; PageState = !PageState;
PageButtonClicked.Invoke(sender, this); PageButtonClicked.Invoke(sender, this);
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void VolumeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) private void VolumeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{ {
Volume = e.NewValue; Volume = e.NewValue;
} }
/// <summary>
///
/// </summary>
/// <param name="propertyName"></param>
protected virtual void OnPropertyChanged(string propertyName) protected virtual void OnPropertyChanged(string propertyName)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void ChannelMarkerBtn_Click(object sender, RoutedEventArgs e) private void ChannelMarkerBtn_Click(object sender, RoutedEventArgs e)
{ {
if (!IsSelected) return; if (!IsSelected)
return;
HoldState = !HoldState; HoldState = !HoldState;
HoldChannelButtonClicked.Invoke(sender, this); HoldChannelButtonClicked.Invoke(sender, this);
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void PttButton_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e) private void PttButton_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
{ {
if (!IsSelected || PttState) return; if (!IsSelected || PttState)
return;
((Button)sender).Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FF3FA0FF")); ((Button)sender).Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FF3FA0FF"));
} }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void PttButton_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e) private void PttButton_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
{ {
if (!IsSelected || PttState) return; if (!IsSelected || PttState)
return;
((Button)sender).Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FFDDDDDD")); ((Button)sender).Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FFDDDDDD"));
} }
} } // public partial class ChannelBox : UserControl, INotifyPropertyChanged
} } // namespace dvmconsole.Controls

@ -1,4 +1,4 @@
<UserControl x:Class="DVMConsole.Controls.SystemStatusBox" <UserControl x:Class="dvmconsole.Controls.SystemStatusBox"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="105" Height="55" Background="#FF0B004B" BorderBrush="Gray" BorderThickness="1"> Width="105" Height="55" Background="#FF0B004B" BorderBrush="Gray" BorderThickness="1">
@ -8,4 +8,5 @@
<TextBlock Text="{Binding ConnectionState}" Foreground="Gold" FontSize="10"/> <TextBlock Text="{Binding ConnectionState}" Foreground="Gold" FontSize="10"/>
</StackPanel> </StackPanel>
</Border> </Border>
</UserControl> </UserControl>

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // 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. * AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * 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) * @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
* *
* Copyright (C) 2025 Caleb, K4PHP * Copyright (C) 2025 Caleb, K4PHP
@ -15,45 +15,84 @@ using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Windows.Controls; using System.Windows.Controls;
namespace DVMConsole.Controls namespace dvmconsole.Controls
{ {
/// <summary>
///
/// </summary>
public partial class SystemStatusBox : UserControl, INotifyPropertyChanged public partial class SystemStatusBox : UserControl, INotifyPropertyChanged
{ {
private string _connectionState = "Disconnected"; private string connectionState = "Disconnected";
/*
** Properties
*/
/// <summary>
///
/// </summary>
public string SystemName { get; set; } public string SystemName { get; set; }
/// <summary>
///
/// </summary>
public string AddressPort { get; set; } public string AddressPort { get; set; }
/// <summary>
///
/// </summary>
public string ConnectionState public string ConnectionState
{ {
get => _connectionState; get => connectionState;
set set
{ {
if (_connectionState != value) if (connectionState != value)
{ {
_connectionState = value; connectionState = value;
NotifyPropertyChanged(); NotifyPropertyChanged();
} }
} }
} }
/*
** Events
*/
/// <summary>
///
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="SystemStatusBox"/> class.
/// </summary>
public SystemStatusBox() public SystemStatusBox()
{ {
InitializeComponent(); InitializeComponent();
DataContext = this; DataContext = this;
} }
/// <summary>
/// Initializes a new instance of the <see cref="SystemStatusBox"/> class.
/// </summary>
/// <param name="systemName"></param>
/// <param name="address"></param>
/// <param name="port"></param>
public SystemStatusBox(string systemName, string address, int port) : this() public SystemStatusBox(string systemName, string address, int port) : this()
{ {
SystemName = systemName; SystemName = systemName;
AddressPort = $"Address: {address}:{port}"; AddressPort = $"Address: {address}:{port}";
} }
public event PropertyChangedEventHandler PropertyChanged; /// <summary>
///
/// </summary>
/// <param name="propertyName"></param>
protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null) protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
} } // public partial class SystemStatusBox : UserControl, INotifyPropertyChanged
} } // namespace dvmconsole.Controls
Loading…
Cancel
Save

Powered by TurnKey Linux.