project cleanup; file reorg; rewrite README.md;

pull/1/head
Bryan Biedenkapp 11 months ago
parent b9e61d58fb
commit f1a57b593b

@ -3,14 +3,14 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34330.188
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DVMConsole", "DVMConsole\DVMConsole.csproj", "{710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dvmconsole", "DVMConsole\dvmconsole.csproj", "{710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B5A7CF60-CCDE-4B2B-85C1-86AE3A19FB31}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "fnecore", "fnecore\fnecore.csproj", "{1F06ECB1-9928-1430-63F4-2E01522A0510}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "fnecore", "fnecore\fnecore.csproj", "{1F06ECB1-9928-1430-63F4-2E01522A0510}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution

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

@ -11,8 +11,10 @@
*
*/
using System.Runtime.InteropServices;
#if WIN32
namespace DVMConsole
namespace dvmconsole
{
/// <summary>
/// Implements P/Invoke to callback into external AMBE encoder/decoder library.

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

@ -1,14 +1,25 @@
using System.Configuration;
using System.Data;
// SPDX-License-Identifier: AGPL-3.0-only
/**
* Digital Voice Modem - Desktop Dispatch Console
* AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* @package DVM / Desktop Dispatch Console
* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
* Copyright (C) 2025 Caleb, K4PHP
*
*/
using System.Windows;
namespace DVMConsole
namespace dvmconsole
{
/// <summary>
/// Interaction logic for App.xaml
///
/// </summary>
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;
[assembly: ThemeInfo(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

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

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

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

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only
/**
* Digital Voice Modem - DVMConsole
* Digital Voice Modem - Desktop Dispatch Console
* AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* @package DVM / DVM Console
* @package DVM / Desktop Dispatch Console
* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
* Copyright (C) 2025 Caleb, K4PHP
@ -15,12 +15,91 @@ using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Media;
namespace DVMConsole
namespace dvmconsole
{
/// <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
{
/*
** Properties
*/
/// <summary>
///
/// </summary>
public CallHistoryViewModel ViewModel { get; set; }
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="CallHistoryWindow"/> class.
/// </summary>
public CallHistoryWindow()
{
InitializeComponent();
@ -28,12 +107,22 @@ namespace DVMConsole
DataContext = ViewModel;
}
/// <summary>
///
/// </summary>
/// <param name="e"></param>
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
e.Cancel = true;
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)
{
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)
{
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)
{
Dispatcher.Invoke(() =>
@ -73,31 +173,5 @@ 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); }
}
}
}
} // public partial class CallHistoryWindow : Window
} // namespace dvmconsole

@ -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
/**
* Digital Voice Modem - DVMConsole
* Digital Voice Modem - Desktop Dispatch Console
* AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* @package DVM / DVM Console
* @package DVM / Desktop Dispatch Console
* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
* Copyright (C) 2024-2025 Caleb, K4PHP
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
*
*/
using System.Security.Policy;
using fnecore.P25;
namespace DVMConsole
namespace dvmconsole
{
/// <summary>
/// Codeplug object used project wide
/// Codeplug object used to configure the console.
/// </summary>
public class Codeplug
{
/*
** Properties
*/
/// <summary>
///
/// </summary>
public List<System> Systems { get; set; }
/// <summary>
///
/// </summary>
public List<Zone> Zones { get; set; }
/*
** Classes
*/
/// <summary>
///
/// </summary>
public class System
{
/*
** Properties
*/
/// <summary>
///
/// </summary>
public string Name { get; set; }
/// <summary>
///
/// </summary>
public string Identity { get; set; }
/// <summary>
///
/// </summary>
public string Address { get; set; }
/// <summary>
///
/// </summary>
public string Password { get; set; }
/// <summary>
///
/// </summary>
public string PresharedKey { get; set; }
/// <summary>
///
/// </summary>
public bool Encrypted { get; set; }
/// <summary>
///
/// </summary>
public uint PeerId { get; set; }
/// <summary>
///
/// </summary>
public int Port { get; set; }
/// <summary>
///
/// </summary>
public string Rid { get; set; }
/// <summary>
///
/// </summary>
public string AliasPath { get; set; } = "./alias.yml";
/// <summary>
///
/// </summary>
public List<RadioAlias> RidAlias { get; set; } = null;
/*
** Methods
*/
/// <summary>
///
/// </summary>
/// <returns></returns>
public override string ToString()
{
return Name;
}
}
} // public class System
/// <summary>
///
/// </summary>
public class Zone
{
/*
** Properties
*/
/// <summary>
///
/// </summary>
public string Name { get; set; }
/// <summary>
///
/// </summary>
public List<Channel> Channels { get; set; }
}
} // public class Zone
/// <summary>
///
/// </summary>
public class Channel
{
/*
** Properties
*/
/// <summary>
///
/// </summary>
public string Name { get; set; }
/// <summary>
///
/// </summary>
public string System { get; set; }
/// <summary>
///
/// </summary>
public string Tgid { get; set; }
/// <summary>
///
/// </summary>
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; }
/*
** Methods
*/
/// <summary>
///
/// </summary>
/// <returns></returns>
public ushort GetKeyId()
{
return Convert.ToUInt16(KeyId, 16);
}
/// <summary>
///
/// </summary>
/// <returns></returns>
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()
{
if (EncryptionKey == null)
return [];
return EncryptionKey
.Split(',')
.Select(s => Convert.ToByte(s.Trim(), 16))
.ToArray();
return EncryptionKey.Split(',').Select(s => Convert.ToByte(s.Trim(), 16)).ToArray();
}
}
} // public class Channel
/// <summary>
/// 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);
if (channel != null)
{
return Systems.FirstOrDefault(s => s.Name == channel.System);
}
}
return null;
}
@ -129,11 +240,10 @@ namespace DVMConsole
{
var channel = zone.Channels.FirstOrDefault(c => c.Name == channelName);
if (channel != null)
{
return channel;
}
}
return null;
}
}
}
} //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>
<Configurations>Debug;Release;WIN32</Configurations>
<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>
<ItemGroup>
@ -28,7 +31,6 @@
<None Remove="Assets\pttselect.png" />
<None Remove="Assets\whackerlink-logo.png" />
<None Remove="Assets\WhackerLinkLogoV2.png" />
<None Remove="Assets\WhackerLinkLogoV4.png" />
<None Remove="clearemerg.png" />
<None Remove="connection.png" />
<None Remove="page.png" />
@ -94,9 +96,6 @@
<Content Include="Assets\pttselect.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Assets\WhackerLinkLogoV4.png">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
@ -121,7 +120,7 @@
</ItemGroup>
<ItemGroup>
<Page Update="ChannelBox.xaml">
<Page Update="Controls\ChannelBox.xaml">
<CopyToOutputDirectory></CopyToOutputDirectory>
</Page>
</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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WhackerLinkConsoleV2"
xmlns:local="clr-namespace:dvmconsole"
mc:Ignorable="d"
Title="P25 Page" Height="200" Width="300" WindowStartupLocation="CenterOwner">
<Grid>

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

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

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

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

@ -15,8 +15,31 @@
using fnecore;
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>
/// Implements a FNE system base.
/// </summary>
@ -55,11 +78,20 @@ namespace DVMConsole
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)
{
CreateP25MessageHdr(duid, callData, ref data);
// if an mi is present, this is an encrypted header
// if an MI is present, this is an encrypted header
if (mi != null)
{
data[14U] |= 0x08; // Control bit
@ -329,25 +361,25 @@ namespace DVMConsole
break;
case P25DFSI.P25_DFSI_LDU2_VOICE12:
{
dfsiFrame[1U] = cryptoParams.Mi[0]; // Message Indicator
dfsiFrame[2U] = cryptoParams.Mi[1];
dfsiFrame[3U] = cryptoParams.Mi[2];
dfsiFrame[1U] = cryptoParams.MI[0]; // Message Indicator
dfsiFrame[2U] = cryptoParams.MI[1];
dfsiFrame[3U] = cryptoParams.MI[2];
Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE
}
break;
case P25DFSI.P25_DFSI_LDU2_VOICE13:
{
dfsiFrame[1U] = cryptoParams.Mi[3]; // Message Indicator
dfsiFrame[2U] = cryptoParams.Mi[4];
dfsiFrame[3U] = cryptoParams.Mi[5];
dfsiFrame[1U] = cryptoParams.MI[3]; // Message Indicator
dfsiFrame[2U] = cryptoParams.MI[4];
dfsiFrame[3U] = cryptoParams.MI[5];
Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE
}
break;
case P25DFSI.P25_DFSI_LDU2_VOICE14:
{
dfsiFrame[1U] = cryptoParams.Mi[6]; // Message Indicator
dfsiFrame[2U] = cryptoParams.Mi[7];
dfsiFrame[3U] = cryptoParams.Mi[8];
dfsiFrame[1U] = cryptoParams.MI[6]; // Message Indicator
dfsiFrame[2U] = cryptoParams.MI[7];
dfsiFrame[3U] = cryptoParams.MI[8];
Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE
}
break;
@ -468,14 +500,4 @@ namespace DVMConsole
}
}
} // public abstract partial class FneSystemBase : fnecore.FneSystemBase
/// <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; }
}
}
} // namespace dvmconsole

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

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

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only
/**
* Digital Voice Modem - DVMConsole
* Digital Voice Modem - Desktop Dispatch Console
* AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* @package DVM / DVM Console
* @package DVM / Desktop Dispatch Console
* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
* Copyright (C) 2025 Caleb, K4PHP
@ -12,40 +12,64 @@
*/
using NAudio.Wave;
using NAudio.Wave.SampleProviders;
using System;
namespace DVMConsole
namespace dvmconsole
{
/// <summary>
///
/// </summary>
public class GainSampleProvider : ISampleProvider
{
private readonly ISampleProvider _source;
private float _gain = 1.0f;
private readonly ISampleProvider source;
private float gain = 1.0f;
public GainSampleProvider(ISampleProvider source)
{
_source = source ?? throw new ArgumentNullException(nameof(source));
WaveFormat = source.WaveFormat;
}
/*
** Properties
*/
/// <summary>
///
/// </summary>
public WaveFormat WaveFormat { get; }
/// <summary>
///
/// </summary>
public float Gain
{
get => _gain;
set => _gain = Math.Max(0, value);
get => gain;
set => gain = Math.Max(0, value);
}
public int Read(float[] buffer, int offset, int count)
/*
** Methods
*/
/// <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++)
{
buffer[offset + i] *= _gain;
}
buffer[offset + i] *= gain;
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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

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

@ -1,15 +1,24 @@
// From github.com/w3axl/rc2-dvm
// SPDX-License-Identifier: AGPL-3.0-only
/**
* Digital Voice Modem - Desktop Dispatch Console
* AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* @package DVM / Desktop Dispatch Console
* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
* Copyright (C) 2025 Patrick McDonnell, W3AXL
*
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NWaves.Signals;
using NWaves.Transforms;
namespace DVMConsole
namespace dvmconsole
{
/// <summary>
///
/// </summary>
public class MBEToneDetector
{
// Samplerate is 8000 Hz
@ -41,6 +50,10 @@ namespace DVMConsole
// The STFT (short-time fourier transform) operator
private Stft stft;
/*
** Methods
*/
/// <summary>
/// Create a pitch detector which reports the running average of pitch for a sequence of samples
/// </summary>
@ -123,5 +136,5 @@ namespace DVMConsole
}
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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DVMConsole.Controls"
Title="DVMConsole" Height="600" Width="1000" Background="#FFF2F2F2">
xmlns:local="clr-namespace:dvmconsole.Controls"
Title="Digital Voice Modem - Desktop Dispatch Console" Height="600" Width="1000" Background="#FFF2F2F2">
<Grid Background="#FFF2F2F2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="497*"/>

File diff suppressed because it is too large Load Diff

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

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only
/**
* Digital Voice Modem - DVMConsole
* Digital Voice Modem - Desktop Dispatch Console
* AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* @package DVM / DVM Console
* @package DVM / Desktop Dispatch Console
* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
* Copyright (C) 2023 Bryan Biedenkapp, N2PLL
@ -16,7 +16,7 @@ using System.Net;
using fnecore;
namespace DVMConsole
namespace dvmconsole
{
/// <summary>
/// Implements a peer FNE router system.
@ -113,4 +113,4 @@ namespace DVMConsole
/* stub */
}
} // 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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DVMConsole"
xmlns:local="clr-namespace:dvmconsole"
mc:Ignorable="d"
Title="Manual QC2 Page" Height="300" Width="300">
<Grid>

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

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

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

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

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only
/**
* Digital Voice Modem - DVMConsole
* Digital Voice Modem - Desktop Dispatch Console
* AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* @package DVM / DVM Console
* @package DVM / Desktop Dispatch Console
* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
* Copyright (C) 2024-2025 Caleb, K4PHP
@ -13,27 +13,31 @@
using NAudio.Wave;
namespace DVMConsole
namespace dvmconsole
{
/// <summary>
///
/// </summary>
public class ToneGenerator
{
private readonly int _sampleRate = 8000;
private readonly int _bitsPerSample = 16;
private readonly int _channels = 1;
private WaveOutEvent _waveOut;
private BufferedWaveProvider _waveProvider;
private readonly int sampleRate = 8000;
private readonly int bitsPerSample = 16;
private readonly int channels = 1;
private WaveOutEvent waveOut;
private BufferedWaveProvider waveProvider;
/*
** Methods
*/
/// <summary>
/// Creates an instance of <see cref="ToneGenerator"/>
/// Initializes a new instance of the <see cref="ToneGenerator"/> class.
/// </summary>
public ToneGenerator()
{
_waveOut = new WaveOutEvent();
_waveProvider = new BufferedWaveProvider(new WaveFormat(_sampleRate, _bitsPerSample, _channels));
_waveOut.Init(_waveProvider);
waveOut = new WaveOutEvent();
waveProvider = new BufferedWaveProvider(new WaveFormat(sampleRate, bitsPerSample, channels));
waveOut.Init(waveProvider);
}
/// <summary>
@ -44,12 +48,12 @@ namespace DVMConsole
/// <returns>PCM data as a byte array</returns>
public byte[] GenerateTone(double frequency, double durationSeconds)
{
int sampleCount = (int)(_sampleRate * durationSeconds);
byte[] buffer = new byte[sampleCount * (_bitsPerSample / 8)];
int sampleCount = (int)(sampleRate * durationSeconds);
byte[] buffer = new byte[sampleCount * (bitsPerSample / 8)];
for (int i = 0; i < sampleCount; i++)
{
double time = (double)i / _sampleRate;
double time = (double)i / sampleRate;
short sampleValue = (short)(Math.Sin(2 * Math.PI * frequency * time) * short.MaxValue);
buffer[i * 2] = (byte)(sampleValue & 0xFF);
@ -68,10 +72,10 @@ namespace DVMConsole
{
byte[] toneData = GenerateTone(frequency, durationSeconds);
_waveProvider.ClearBuffer();
_waveProvider.AddSamples(toneData, 0, toneData.Length);
waveProvider.ClearBuffer();
waveProvider.AddSamples(toneData, 0, toneData.Length);
_waveOut.Play();
waveOut.Play();
}
/// <summary>
@ -79,7 +83,7 @@ namespace DVMConsole
/// </summary>
public void StopTone()
{
_waveOut.Stop();
waveOut.Stop();
}
/// <summary>
@ -87,7 +91,7 @@ namespace DVMConsole
/// </summary>
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.Text;
using System.Threading.Tasks;
using fnecore;
namespace DVMConsole
namespace dvmconsole
{
/// <summary>
///
/// </summary>
public enum MBE_MODE
{
DMR_AMBE, //! DMR AMBE
IMBE_88BIT, //! 88-bit IMBE (P25)
}
} // public enum MBE_MODE
/// <summary>
/// Wrapper class for the c++ dvmvocoder encoder library
/// Wrapper class for the C++ dvmvocoder encoder library.
/// </summary>
/// Using info from https://stackoverflow.com/a/315064/1842613
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>
/// Create a new MBEEncoder
/// </summary>
@ -54,28 +84,6 @@ namespace DVMConsole
[DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)]
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>
/// Encode PCM16 samples to MBE codeword
/// </summary>
@ -86,17 +94,45 @@ namespace DVMConsole
MBEEncoder_Encode(encoder, samples, codeword);
}
/// <summary>
///
/// </summary>
/// <param name="bits"></param>
/// <param name="codeword"></param>
public void encodeBits([In] char[] bits, [Out] byte[] codeword)
{
MBEEncoder_EncodeBits(encoder, bits, codeword);
}
}
} // public class MBEEncoder
/// <summary>
/// Wrapper class for the c++ dvmvocoder decoder library
/// Wrapper class for the C++ dvmvocoder decoder library.
/// </summary>
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>
/// Create a new MBEDecoder
/// </summary>
@ -130,28 +166,6 @@ namespace DVMConsole
[DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)]
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>
/// Decode MBE codeword to PCM16 samples
/// </summary>
@ -172,8 +186,11 @@ namespace DVMConsole
{
return MBEDecoder_DecodeBits(decoder, codeword, bits);
}
}
} // public class MBEDecoder
/// <summary>
///
/// </summary>
public static class MBEToneGenerator
{
/// <summary>
@ -183,7 +200,7 @@ namespace DVMConsole
/// <param name="tone_amplitude"></param>
/// <param name="codeword"></param>
/// <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
// u0 and u1 are 12 bits
@ -197,15 +214,11 @@ namespace DVMConsole
// Validate tone index
if (tone_idx < 5 || tone_idx > 122)
{
throw new ArgumentOutOfRangeException($"Tone index for frequency out of range!");
}
// Validate amplitude value
if (tone_amplitude > 127)
{
throw new ArgumentOutOfRangeException("Tone amplitude must be between 0 and 127!");
}
// Make sure tone index only has 7 bits (it should but we make sure :) )
tone_idx &= 0b01111111;
@ -258,8 +271,11 @@ namespace DVMConsole
byte[] tone_codeword = VocoderToneLookupTable.IMBEToneFrames[nearest];
Array.Copy(tone_codeword, codeword, tone_codeword.Length);
}
}
} // public static class MBEToneGenerator
/// <summary>
///
/// </summary>
public class MBEInterleaver
{
public const int PCM_SAMPLES = 160;
@ -273,6 +289,14 @@ namespace DVMConsole
private MBEEncoder encoder;
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)
{
this.mode = mode;
@ -280,13 +304,19 @@ namespace DVMConsole
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
if (codeword == null)
{
throw new NullReferenceException("Input MBE codeword is null!");
}
char[] bits = null;
@ -294,24 +324,18 @@ namespace DVMConsole
if (mode == MBE_MODE.DMR_AMBE)
{
if (codeword.Length != AMBE_CODEWORD_SAMPLES)
{
throw new ArgumentOutOfRangeException($"AMBE codeword length is != {AMBE_CODEWORD_SAMPLES}");
}
bits = new char[AMBE_CODEWORD_BITS];
}
else if (mode == MBE_MODE.IMBE_88BIT)
{
if (codeword.Length != IMBE_CODEWORD_SAMPLES)
{
throw new ArgumentOutOfRangeException($"IMBE codeword length is != {IMBE_CODEWORD_SAMPLES}");
}
bits = new char[IMBE_CODEWORD_BITS];
}
if (bits == null)
{
throw new NullReferenceException("Failed to initialize decoder");
}
// Decode
int errs = decoder.decodeBits(codeword, bits);
@ -334,12 +358,18 @@ namespace DVMConsole
return errs;
}
/// <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)
{
if (mbeBits == null)
{
throw new NullReferenceException("Input MBE bit array is null!");
}
char[] bits = null;
@ -347,34 +377,30 @@ namespace DVMConsole
if (mode == MBE_MODE.DMR_AMBE)
{
if (mbeBits.Length != AMBE_CODEWORD_BITS)
{
throw new ArgumentOutOfRangeException($"AMBE codeword bit length is != {AMBE_CODEWORD_BITS}");
}
bits = new char[AMBE_CODEWORD_BITS];
Array.Copy(mbeBits, bits, AMBE_CODEWORD_BITS);
}
else if (mode == MBE_MODE.IMBE_88BIT)
{
if (mbeBits.Length != IMBE_CODEWORD_BITS)
{
throw new ArgumentOutOfRangeException($"IMBE codeword bit length is != {AMBE_CODEWORD_BITS}");
}
bits = new char[IMBE_CODEWORD_BITS];
Array.Copy(mbeBits, bits, IMBE_CODEWORD_BITS);
}
if (bits == null)
{
throw new ArgumentException("Bit array did not get set up properly!");
}
// Encode samples
if (mode == MBE_MODE.DMR_AMBE)
{
// Create output array
byte[] codewords = new byte[AMBE_CODEWORD_SAMPLES];
// Encode
encoder.encodeBits(bits, codewords);
// Copy
codeword = new byte[AMBE_CODEWORD_SAMPLES];
Array.Copy(codewords, codeword, IMBE_CODEWORD_SAMPLES);
@ -383,12 +409,14 @@ namespace DVMConsole
{
// Create output array
byte[] codewords = new byte[IMBE_CODEWORD_SAMPLES];
// Encode
encoder.encodeBits(bits, codewords);
// Copy
codeword = new byte[IMBE_CODEWORD_SAMPLES];
Array.Copy(codewords, codeword, IMBE_CODEWORD_SAMPLES);
}
}
}
}
} // 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>
/// From https://github.com/W3AXL/rc2-dvm/blob/main/rc2-dvm/Audio.cs
@ -84,5 +97,5 @@
{ 2469, new byte[] { 0x5, 0x6, 0xFB, 0x63, 0xCD, 0xD9, 0x2B, 0x42, 0xE1, 0xCF, 0x6D } },
{ 2500, new byte[] { 0x5, 0x6, 0xFB, 0x63, 0xCD, 0xD9, 0x2B, 0x42, 0xE1, 0xCF, 0x6B } },
};
}
}
} // public class VocoderToneLookupTable
} // namespace dvmconsole

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

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

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

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

@ -1,12 +1,18 @@
# DVMConsole
### DVM Desktop Console
![console](./images/consolehome.JPG)
# Digital Voice Modem Desktop Dispatch Console
This provides a desktop application that mimics or otherwise operates like a system dispatch console.
## Setup
- Download the packaged release from the releases or clone and build yourself
- Modify the codeplug file
- Select the codeplug once opening the app
## Features
- Custumizable widgets
- Individual channel audio control
- AES and RC4 crypto support
- Auto saved and transferable user settings
## 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:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="55" Width="105"
@ -26,9 +26,6 @@
</Button.Background>
<Image Height="40" Width="63" Source="/Assets/page.png" Margin="0,-8,0,0"/>
</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>
</UserControl>

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

@ -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:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="220" Height="100" Background="#FF0B004B">
@ -42,9 +42,9 @@
<!-- Volume Slider -->
<Slider Minimum="0" Maximum="4" Value="{Binding Volume, Mode=TwoWay}"
Height="21" VerticalAlignment="Top" x:Name="VolumeSlider"
ValueChanged="VolumeSlider_ValueChanged" Margin="11,10,65,0"
Grid.ColumnSpan="2" Grid.Row="2">
Height="21" VerticalAlignment="Top" x:Name="VolumeSlider"
ValueChanged="VolumeSlider_ValueChanged" Margin="11,10,65,0"
Grid.ColumnSpan="2" Grid.Row="2">
<Slider.Style>
<Style TargetType="Slider">
<Setter Property="Template">
@ -77,8 +77,6 @@
</Slider.Style>
</Slider>
<!-- Bottom Buttons -->
<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">

@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only
/**
* Digital Voice Modem - DVMConsole
* Digital Voice Modem - Desktop Dispatch Console
* AGPLv3 Open Source. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* @package DVM / DVM Console
* @package DVM / Desktop Dispatch Console
* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
* Copyright (C) 2025 Caleb, K4PHP
@ -12,38 +12,36 @@
*/
using System.ComponentModel;
using System.Security.Cryptography;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using fnecore.P25;
namespace DVMConsole.Controls
namespace dvmconsole.Controls
{
/// <summary>
///
/// </summary>
public partial class ChannelBox : UserControl, INotifyPropertyChanged
{
private readonly SelectedChannelsManager _selectedChannelsManager;
private readonly AudioManager _audioManager;
private readonly SelectedChannelsManager selectedChannelsManager;
private readonly AudioManager audioManager;
private bool _pttState;
private bool _pageState;
private bool _holdState;
private bool _emergency;
private string _lastSrcId = "0";
private double _volume = 1.0;
private bool pttState;
private bool pageState;
private bool holdState;
private bool emergency;
private string lastSrcId = "0";
private double volume = 1.0;
private bool isSelected;
internal LinearGradientBrush grayGradient;
internal LinearGradientBrush redGradient;
internal LinearGradientBrush orangeGradient;
public FlashingBackgroundManager _flashingBackgroundManager;
public event EventHandler<ChannelBox> PTTButtonClicked;
public event EventHandler<ChannelBox> PageButtonClicked;
public event EventHandler<ChannelBox> HoldChannelButtonClicked;
public event PropertyChangedEventHandler PropertyChanged;
public FlashingBackgroundManager flashingBackgroundManager;
public byte[] netLDU1 = 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 string ChannelName { get; set; }
public string SystemName { get; set; }
public string DstId { get; set; }
#if WIN32
public AmbeVocoder extFullRateVocoder;
public AmbeVocoder extHalfRateVocoder;
@ -73,111 +67,196 @@ namespace DVMConsole.Controls
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;
/// <summary>
///
/// </summary>
public bool IsReceivingEncrypted { get; set; } = false;
/// <summary>
///
/// </summary>
public string LastSrcId
{
get => _lastSrcId;
get => lastSrcId;
set
{
if (_lastSrcId != value)
if (lastSrcId != value)
{
_lastSrcId = value;
lastSrcId = value;
OnPropertyChanged(nameof(LastSrcId));
}
}
}
/// <summary>
///
/// </summary>
public bool PttState
{
get => _pttState;
get => pttState;
set
{
_pttState = value;
pttState = value;
UpdatePTTColor();
}
}
/// <summary>
///
/// </summary>
public bool PageState
{
get => _pageState;
get => pageState;
set
{
_pageState = value;
pageState = value;
UpdatePageColor();
}
}
/// <summary>
///
/// </summary>
public bool HoldState
{
get => _holdState;
get => holdState;
set
{
_holdState = value;
holdState = value;
UpdateHoldColor();
}
}
/// <summary>
///
/// </summary>
public bool Emergency
{
get => _emergency;
get => emergency;
set
{
_emergency = value;
emergency = value;
Dispatcher.Invoke(() =>
{
if (value)
_flashingBackgroundManager.Start();
flashingBackgroundManager.Start();
else
_flashingBackgroundManager.Stop();
flashingBackgroundManager.Stop();
});
}
}
/// <summary>
///
/// </summary>
public string VoiceChannel { get; set; }
/// <summary>
///
/// </summary>
public bool IsEditMode { get; set; }
private bool _isSelected;
/// <summary>
///
/// </summary>
public bool IsSelected
{
get => _isSelected;
get => isSelected;
set
{
_isSelected = value;
isSelected = value;
UpdateBackground();
}
}
/// <summary>
///
/// </summary>
public double Volume
{
get => _volume;
get => volume;
set
{
if (_volume != value)
if (volume != value)
{
_volume = value;
volume = value;
OnPropertyChanged(nameof(Volume));
_audioManager.SetTalkgroupVolume(DstId, (float)value);
audioManager.SetTalkgroupVolume(DstId, (float)value);
}
}
}
/// <summary>
///
/// </summary>
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)
{
InitializeComponent();
DataContext = this;
_selectedChannelsManager = selectedChannelsManager;
_audioManager = audioManager;
_flashingBackgroundManager = new FlashingBackgroundManager(this);
this.selectedChannelsManager = selectedChannelsManager;
this.audioManager = audioManager;
flashingBackgroundManager = new FlashingBackgroundManager(this);
ChannelName = channelName;
DstId = dstId;
SystemName = $"System: {systemName}";
LastSrcId = $"Last SRC: {LastSrcId}";
LastSrcId = $"Last ID: {LastSrcId}";
UpdateBackground();
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)
{
if (IsEditMode) return;
if (IsEditMode)
return;
IsSelected = !IsSelected;
Background = IsSelected ? (Brush)new BrushConverter().ConvertFrom("#FF0B004B") : Brushes.Gray;
if (IsSelected)
{
_selectedChannelsManager.AddSelectedChannel(this);
}
selectedChannelsManager.AddSelectedChannel(this);
else
{
_selectedChannelsManager.RemoveSelectedChannel(this);
}
selectedChannelsManager.RemoveSelectedChannel(this);
}
/// <summary>
///
/// </summary>
private void UpdatePTTColor()
{
if (IsEditMode) return;
if (IsEditMode)
return;
if (PttState)
PttButton.Background = redGradient;
@ -247,9 +332,13 @@ namespace DVMConsole.Controls
PttButton.Background = grayGradient;
}
/// <summary>
///
/// </summary>
private void UpdatePageColor()
{
if (IsEditMode) return;
if (IsEditMode)
return;
if (PageState)
PageSelectButton.Background = orangeGradient;
@ -259,7 +348,8 @@ namespace DVMConsole.Controls
private void UpdateHoldColor()
{
if (IsEditMode) return;
if (IsEditMode)
return;
if (HoldState)
ChannelMarkerBtn.Background = orangeGradient;
@ -267,6 +357,9 @@ namespace DVMConsole.Controls
ChannelMarkerBtn.Background = grayGradient;
}
/// <summary>
///
/// </summary>
private void UpdateBackground()
{
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;
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void PTTButton_Click(object sender, RoutedEventArgs e)
{
if (!IsSelected) return;
if (!IsSelected)
return;
if (PttState)
await Task.Delay(500);
@ -290,44 +389,77 @@ namespace DVMConsole.Controls
PTTButtonClicked.Invoke(sender, this);
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void PageSelectButton_Click(object sender, RoutedEventArgs e)
{
if (!IsSelected) return;
if (!IsSelected)
return;
PageState = !PageState;
PageButtonClicked.Invoke(sender, this);
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void VolumeSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
Volume = e.NewValue;
}
/// <summary>
///
/// </summary>
/// <param name="propertyName"></param>
protected virtual void OnPropertyChanged(string 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)
{
if (!IsSelected) return;
if (!IsSelected)
return;
HoldState = !HoldState;
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)
{
if (!IsSelected || PttState) return;
if (!IsSelected || PttState)
return;
((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)
{
if (!IsSelected || PttState) return;
if (!IsSelected || PttState)
return;
((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:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="105" Height="55" Background="#FF0B004B" BorderBrush="Gray" BorderThickness="1">
@ -9,3 +9,4 @@
</StackPanel>
</Border>
</UserControl>

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

Powered by TurnKey Linux.