diff --git a/dvmconsole/Assets/bg_main_hd_dark.png b/dvmconsole/Assets/bg_main_hd_dark.png new file mode 100644 index 0000000..0eee0f9 Binary files /dev/null and b/dvmconsole/Assets/bg_main_hd_dark.png differ diff --git a/dvmconsole/Assets/bg_main_hd.png b/dvmconsole/Assets/bg_main_hd_light.png similarity index 100% rename from dvmconsole/Assets/bg_main_hd.png rename to dvmconsole/Assets/bg_main_hd_light.png diff --git a/dvmconsole/AudioSettingsWindow.xaml.cs b/dvmconsole/AudioSettingsWindow.xaml.cs index 011628d..9cc89bc 100644 --- a/dvmconsole/AudioSettingsWindow.xaml.cs +++ b/dvmconsole/AudioSettingsWindow.xaml.cs @@ -132,6 +132,8 @@ namespace dvmconsole return outputDevices; } + /** WPF Events */ + /// /// /// diff --git a/dvmconsole/CallHistoryWindow.xaml.cs b/dvmconsole/CallHistoryWindow.xaml.cs index 98c4dae..ec9ee6a 100644 --- a/dvmconsole/CallHistoryWindow.xaml.cs +++ b/dvmconsole/CallHistoryWindow.xaml.cs @@ -18,34 +18,7 @@ using System.Windows.Media; namespace dvmconsole { /// - /// - /// - public class CallHistoryViewModel - { - /* - ** Properties - */ - - /// - /// - /// - public ObservableCollection CallHistory { get; set; } - - /* - ** Methods - */ - - /// - /// Initializes a new instance of the class. - /// - public CallHistoryViewModel() - { - CallHistory = new ObservableCollection(); - } - } // public class CallHistoryViewModel - - /// - /// + /// Data structure representing a call entry. /// public class CallEntry : DependencyObject { @@ -57,20 +30,20 @@ namespace dvmconsole */ /// - /// + /// Textual name of channel call was received on. /// public string Channel { get; set; } /// - /// + /// Source ID. /// public int SrcId { get; set; } /// - /// + /// Destination ID. /// public int DstId { get; set; } /// - /// + /// Background color for call entry. /// public Brush BackgroundColor { @@ -79,6 +52,33 @@ namespace dvmconsole } } // public class CallEntry : DependencyObject + /// + /// Data view model representing the call history. + /// + public class CallHistoryViewModel + { + /* + ** Properties + */ + + /// + /// Collection of call history entries. + /// + public ObservableCollection CallHistory { get; set; } + + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// + public CallHistoryViewModel() + { + CallHistory = new ObservableCollection(); + } + } // public class CallHistoryViewModel + /// /// Interaction logic for CallHistoryWindow.xaml. /// @@ -89,7 +89,7 @@ namespace dvmconsole */ /// - /// + /// Gets or sets the view model for the window. /// public CallHistoryViewModel ViewModel { get; set; } diff --git a/dvmconsole/Controls/ChannelBox.xaml.cs b/dvmconsole/Controls/ChannelBox.xaml.cs index 813ef97..c49f6d3 100644 --- a/dvmconsole/Controls/ChannelBox.xaml.cs +++ b/dvmconsole/Controls/ChannelBox.xaml.cs @@ -13,7 +13,6 @@ */ using System.ComponentModel; -using System.Diagnostics; using System.IO; using System.Reflection; using System.Windows; @@ -78,15 +77,15 @@ namespace dvmconsole.Controls */ /// - /// + /// Textual name of channel. /// public string ChannelName { get; set; } /// - /// + /// Textual name of system channel belongs to. /// public string SystemName { get; set; } /// - /// + /// Destination ID. /// public string DstId { get; set; } @@ -104,33 +103,33 @@ namespace dvmconsole.Controls */ /// - /// + /// Event action that handles the PTT button being clicked. /// public event EventHandler PTTButtonClicked; /// - /// + /// Event action that handles the page button being clicked. /// public event EventHandler PageButtonClicked; /// - /// + /// Event action that handles the hold channel button being clicked. /// public event EventHandler HoldChannelButtonClicked; /// - /// + /// Event action that occurs when a property changes on this control. /// public event PropertyChangedEventHandler PropertyChanged; /// - /// + /// Flag indicating whether or not this channel is receiving. /// public bool IsReceiving { get; set; } = false; /// - /// + /// Flag indicating whether or not this channel is receiving encrypted. /// public bool IsReceivingEncrypted { get; set; } = false; /// - /// + /// Last Source ID received. /// public string LastSrcId { @@ -146,7 +145,7 @@ namespace dvmconsole.Controls } /// - /// + /// Flag indicating the current PTT state of this channel. /// public bool PttState { @@ -159,7 +158,7 @@ namespace dvmconsole.Controls } /// - /// + /// Flag indicating the current page state of this channel. /// public bool PageState { @@ -172,7 +171,7 @@ namespace dvmconsole.Controls } /// - /// + /// Flag indicating the hold state of this channel. /// public bool HoldState { @@ -185,7 +184,7 @@ namespace dvmconsole.Controls } /// - /// + /// Flag indicating the emergency state of this channel. /// public bool Emergency { @@ -210,12 +209,12 @@ namespace dvmconsole.Controls public string VoiceChannel { get; set; } /// - /// + /// Flag indicating whether or not edit mode is enabled. /// public bool IsEditMode { get; set; } /// - /// + /// Flag indicating whether or not this channel is selected. /// public bool IsSelected { @@ -228,7 +227,7 @@ namespace dvmconsole.Controls } /// - /// + /// Current volume for this channel. /// public double Volume { @@ -247,7 +246,7 @@ namespace dvmconsole.Controls /// /// /// - public uint txStreamId { get; internal set; } + public uint TxStreamId { get; internal set; } /* ** Methods @@ -367,31 +366,12 @@ namespace dvmconsole.Controls } } - /// - /// - /// - /// - /// - private void ChannelBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) - { - if (IsEditMode) - return; - - IsSelected = !IsSelected; - ControlBorder.Background = IsSelected ? BLUE_GRADIENT : DARK_GRAY_GRADIENT; - - if (IsSelected) - selectedChannelsManager.AddSelectedChannel(this); - else - selectedChannelsManager.RemoveSelectedChannel(this); - } - /// /// /// private void UpdatePTTColor() { - if (IsEditMode) + if (IsEditMode) return; if (PttState) @@ -405,7 +385,7 @@ namespace dvmconsole.Controls /// private void UpdatePageColor() { - if (IsEditMode) + if (IsEditMode) return; if (PageState) @@ -416,7 +396,7 @@ namespace dvmconsole.Controls private void UpdateHoldColor() { - if (IsEditMode) + if (IsEditMode) return; if (HoldState) @@ -439,6 +419,36 @@ namespace dvmconsole.Controls ControlBorder.Background = IsSelected ? BLUE_GRADIENT : DARK_GRAY_GRADIENT; } + /// + /// + /// + /// + protected virtual void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + /** WPF Events */ + + /// + /// + /// + /// + /// + private void ChannelBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + if (IsEditMode) + return; + + IsSelected = !IsSelected; + ControlBorder.Background = IsSelected ? BLUE_GRADIENT : DARK_GRAY_GRADIENT; + + if (IsSelected) + selectedChannelsManager.AddSelectedChannel(this); + else + selectedChannelsManager.RemoveSelectedChannel(this); + } + /// /// /// @@ -481,15 +491,6 @@ namespace dvmconsole.Controls Volume = e.NewValue; } - /// - /// - /// - /// - protected virtual void OnPropertyChanged(string propertyName) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - /// /// /// diff --git a/dvmconsole/Controls/SystemStatusBox.xaml.cs b/dvmconsole/Controls/SystemStatusBox.xaml.cs index e767a66..e11875e 100644 --- a/dvmconsole/Controls/SystemStatusBox.xaml.cs +++ b/dvmconsole/Controls/SystemStatusBox.xaml.cs @@ -30,16 +30,16 @@ namespace dvmconsole.Controls */ /// - /// + /// Textual name of the system. /// public string SystemName { get; set; } /// - /// + /// Address and port of the system. /// public string AddressPort { get; set; } /// - /// + /// Connection state. /// public string ConnectionState { @@ -68,7 +68,7 @@ namespace dvmconsole.Controls */ /// - /// + /// Event action that occurs when a property changes on this control. /// public event PropertyChangedEventHandler PropertyChanged; diff --git a/dvmconsole/DVMConsole.csproj b/dvmconsole/DVMConsole.csproj index 6111456..45c7c32 100644 --- a/dvmconsole/DVMConsole.csproj +++ b/dvmconsole/DVMConsole.csproj @@ -29,7 +29,8 @@ - + + @@ -62,7 +63,10 @@ Never - + + Never + + Never diff --git a/dvmconsole/DigitalPageWindow.xaml.cs b/dvmconsole/DigitalPageWindow.xaml.cs index 014b207..527f055 100644 --- a/dvmconsole/DigitalPageWindow.xaml.cs +++ b/dvmconsole/DigitalPageWindow.xaml.cs @@ -20,9 +20,15 @@ namespace dvmconsole /// public partial class DigitalPageWindow : Window { - public List systems = new List(); + private List systems = new List(); + /// + /// Destination ID. + /// public string DstId = string.Empty; + /// + /// System. + /// public Codeplug.System RadioSystem = null; /// @@ -39,6 +45,8 @@ namespace dvmconsole SystemCombo.SelectedIndex = 0; } + /** WPF Events */ + /// /// /// diff --git a/dvmconsole/KeyStatusWindow.xaml.cs b/dvmconsole/KeyStatusWindow.xaml.cs index 8dc46f2..951991c 100644 --- a/dvmconsole/KeyStatusWindow.xaml.cs +++ b/dvmconsole/KeyStatusWindow.xaml.cs @@ -20,7 +20,7 @@ using dvmconsole.Controls; namespace dvmconsole { /// - /// + /// Data structure representing a key status item. /// public class KeyStatusItem { @@ -29,25 +29,25 @@ namespace dvmconsole */ /// - /// + /// Textual name of channel key is for. /// public string ChannelName { get; set; } /// - /// + /// Algorithm ID. /// public string AlgId { get; set; } /// - /// + /// Key ID. /// public string KeyId { get; set; } /// - /// + /// Key status. /// public string KeyStatus { get; set; } } // public class KeyStatusItem /// - /// + /// Interaction logic for KeyStatusWindow.xaml. /// public partial class KeyStatusWindow : Window { @@ -59,7 +59,7 @@ namespace dvmconsole */ /// - /// + /// Collection of key status entries. /// public ObservableCollection KeyStatusItems { get; private set; } = new ObservableCollection(); diff --git a/dvmconsole/MainWindow.xaml b/dvmconsole/MainWindow.xaml index 89a4a79..115aff5 100644 --- a/dvmconsole/MainWindow.xaml +++ b/dvmconsole/MainWindow.xaml @@ -51,13 +51,16 @@ + + + - + diff --git a/dvmconsole/MainWindow.xaml.cs b/dvmconsole/MainWindow.xaml.cs index d23513a..f6c8881 100644 --- a/dvmconsole/MainWindow.xaml.cs +++ b/dvmconsole/MainWindow.xaml.cs @@ -19,7 +19,7 @@ using System.Timers; using System.Windows; using System.Windows.Controls; using System.Windows.Input; -using System.Windows.Media; +using System.Windows.Media.Imaging; using Microsoft.Win32; @@ -40,7 +40,7 @@ using fnecore.P25.KMM; namespace dvmconsole { /// - /// + /// Data structure representing the position of a widget. /// public class ChannelPosition { @@ -49,11 +49,11 @@ namespace dvmconsole */ /// - /// + /// X /// public double X { get; set; } /// - /// + /// Y /// public double Y { get; set; } } // public class ChannelPosition @@ -66,6 +66,8 @@ namespace dvmconsole public const double MIN_WIDTH = 875; public const double MIN_HEIGHT = 700; + private const string URI_RESOURCE_PATH = "pack://application:,,,/dvmconsole;component"; + private bool isEditMode = false; private bool globalPttState = false; @@ -108,7 +110,7 @@ namespace dvmconsole */ /// - /// + /// Codeplug /// public Codeplug Codeplug { get; set; } @@ -153,16 +155,22 @@ namespace dvmconsole } /// - /// Helper to enable form controls when settings and codeplug are loaded. + /// Helper to enable menu controls for Commands submenu. /// - private void EnableControls() + private void EnableCommandControls() { menuPageSubscriber.IsEnabled = true; menuRadioCheckSubscriber.IsEnabled = true; menuInhibitSubscriber.IsEnabled = true; menuUninhibitSubscriber.IsEnabled = true; menuQuickCall2.IsEnabled = true; + } + /// + /// Helper to enable form controls when settings and codeplug are loaded. + /// + private void EnableControls() + { btnGlobalPtt.IsEnabled = true; btnAlert1.IsEnabled = true; btnAlert2.IsEnabled = true; @@ -174,15 +182,23 @@ namespace dvmconsole } /// - /// Helper to disable form controls when settings load fails. + /// Helper to disable menu controls for Commands submenu. /// - private void DisableControls() + private void DisableCommandControls() { menuPageSubscriber.IsEnabled = false; menuRadioCheckSubscriber.IsEnabled = false; menuInhibitSubscriber.IsEnabled = false; menuUninhibitSubscriber.IsEnabled = false; menuQuickCall2.IsEnabled = false; + } + + /// + /// Helper to disable form controls when settings load fails. + /// + private void DisableControls() + { + DisableCommandControls(); btnGlobalPtt.IsEnabled = false; btnAlert1.IsEnabled = false; @@ -195,46 +211,7 @@ namespace dvmconsole } /// - /// - /// - /// - /// - private void OpenCodeplug_Click(object sender, RoutedEventArgs e) - { - OpenFileDialog openFileDialog = new OpenFileDialog - { - Filter = "Codeplug Files (*.yml)|*.yml|All Files (*.*)|*.*", - Title = "Open Codeplug" - }; - - if (openFileDialog.ShowDialog() == true) - { - LoadCodeplug(openFileDialog.FileName); - - settingsManager.LastCodeplugPath = openFileDialog.FileName; - noSaveSettingsOnClose = false; - settingsManager.SaveSettings(); - } - } - - /// - /// - /// - /// - /// - private void ResetSettings_Click(object sender, RoutedEventArgs e) - { - var confirmResult = MessageBox.Show("Are you sure to wish to reset console settings?", "Reset Settings", MessageBoxButton.YesNo, MessageBoxImage.Question); - if (confirmResult == MessageBoxResult.Yes) - { - MessageBox.Show("Settings will be reset after console restart.", "Reset Settings", MessageBoxButton.OK, MessageBoxImage.Information); - noSaveSettingsOnClose = true; - settingsManager.Reset(); - } - } - - /// - /// + /// Helper to load the codeplug. /// /// private void LoadCodeplug(string filePath) @@ -260,7 +237,7 @@ namespace dvmconsole } /// - /// + /// Helper to initialize and generate channel widgets on the canvas. /// private void GenerateChannelWidgets() { @@ -435,61 +412,6 @@ namespace dvmconsole Cursor = Cursors.Arrow; } - /// - /// - /// - /// - /// - private void WaveIn_RecordingStopped(object sender, EventArgs e) - { - /* stub */ - } - - /// - /// - /// - /// - /// - private void WaveIn_DataAvailable(object sender, WaveInEventArgs e) - { - bool isAnyTgOn = false; - - foreach (ChannelBox channel in selectedChannelsManager.GetSelectedChannels()) - { - if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG) - continue; - - Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); - Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); - - - PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); - - if (channel.IsSelected && channel.PttState) - { - isAnyTgOn = true; - - int samples = 320; - - Task.Run(() => - { - channel.chunkedPcm = AudioConverter.SplitToChunks(e.Buffer); - - foreach (byte[] chunk in channel.chunkedPcm) - { - if (chunk.Length == samples) - P25EncodeAudioFrame(chunk, handler, channel, cpgChannel, system); - else - Trace.WriteLine("bad sample length: " + chunk.Length); - } - }); - } - } - - if (playbackChannelBox != null && isAnyTgOn && playbackChannelBox.IsSelected) - audioManager.AddTalkgroupStream(PLAYBACKTG, e.Buffer); - } - /// /// /// @@ -518,303 +440,94 @@ namespace dvmconsole /// /// /// - /// /// - private void AudioSettings_Click(object sender, RoutedEventArgs e) + private void SendAlertTone(AlertTone e) { - List channels = Codeplug?.Zones.SelectMany(z => z.Channels).ToList() ?? new List(); - - AudioSettingsWindow audioSettingsWindow = new AudioSettingsWindow(settingsManager, audioManager, channels); - audioSettingsWindow.ShowDialog(); + Task.Run(() => SendAlertTone(e.AlertFilePath)); } /// /// /// - /// - /// - private void PageRID_Click(object sender, RoutedEventArgs e) + /// + /// + private void SendAlertTone(string filePath, bool forHold = false) { - DigitalPageWindow pageWindow = new DigitalPageWindow(Codeplug.Systems); - pageWindow.Owner = this; - pageWindow.Title = "Page Subscriber"; - - if (pageWindow.ShowDialog() == true) + if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath)) { - PeerSystem handler = fneSystemManager.GetFneSystem(pageWindow.RadioSystem.Name); - IOSP_CALL_ALRT callAlert = new IOSP_CALL_ALRT(uint.Parse(pageWindow.DstId), uint.Parse(pageWindow.RadioSystem.Rid)); - - RemoteCallData callData = new RemoteCallData + try { - SrcId = uint.Parse(pageWindow.RadioSystem.Rid), - DstId = uint.Parse(pageWindow.DstId), - LCO = P25Defines.TSBK_IOSP_CALL_ALRT - }; + foreach (ChannelBox channel in selectedChannelsManager.GetSelectedChannels()) + { + if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG) + continue; - byte[] tsbk = new byte[P25Defines.P25_TSBK_LENGTH_BYTES]; + Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); + Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); + PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); - callAlert.Encode(ref tsbk); + if (channel.PageState || (forHold && channel.HoldState)) + { + byte[] pcmData; - handler.SendP25TSBK(callData, tsbk); - } - } + Task.Run(async () => { + using (var waveReader = new WaveFileReader(filePath)) + { + if (waveReader.WaveFormat.Encoding != WaveFormatEncoding.Pcm || + waveReader.WaveFormat.SampleRate != 8000 || + waveReader.WaveFormat.BitsPerSample != 16 || + waveReader.WaveFormat.Channels != 1) + { + MessageBox.Show("The alert tone must be PCM 16-bit, Mono, 8000Hz format.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); + return; + } - /// - /// - /// - /// - /// - private void RadioCheckRID_Click(object sender, RoutedEventArgs e) - { - DigitalPageWindow pageWindow = new DigitalPageWindow(Codeplug.Systems); - pageWindow.Owner = this; - pageWindow.Title = "Radio Check Subscriber"; + using (MemoryStream ms = new MemoryStream()) + { + waveReader.CopyTo(ms); + pcmData = ms.ToArray(); + } + } - if (pageWindow.ShowDialog() == true) - { - PeerSystem handler = fneSystemManager.GetFneSystem(pageWindow.RadioSystem.Name); - IOSP_EXT_FNCT extFunc = new IOSP_EXT_FNCT((ushort)ExtendedFunction.CHECK, uint.Parse(pageWindow.RadioSystem.Rid), uint.Parse(pageWindow.DstId)); + int chunkSize = 1600; + int totalChunks = (pcmData.Length + chunkSize - 1) / chunkSize; - RemoteCallData callData = new RemoteCallData - { - SrcId = uint.Parse(pageWindow.RadioSystem.Rid), - DstId = uint.Parse(pageWindow.DstId), - LCO = P25Defines.TSBK_IOSP_EXT_FNCT - }; + if (pcmData.Length % chunkSize != 0) + { + byte[] paddedData = new byte[totalChunks * chunkSize]; + Buffer.BlockCopy(pcmData, 0, paddedData, 0, pcmData.Length); + pcmData = paddedData; + } - byte[] tsbk = new byte[P25Defines.P25_TSBK_LENGTH_BYTES]; + Task.Run(() => + { + audioManager.AddTalkgroupStream(cpgChannel.Tgid, pcmData); + }); - extFunc.Encode(ref tsbk); + DateTime startTime = DateTime.UtcNow; - handler.SendP25TSBK(callData, tsbk); - } - } + for (int i = 0; i < totalChunks; i++) + { + int offset = i * chunkSize; + byte[] chunk = new byte[chunkSize]; + Buffer.BlockCopy(pcmData, offset, chunk, 0, chunkSize); - /// - /// - /// - /// - /// - private void InhibitRID_Click(object sender, RoutedEventArgs e) - { - DigitalPageWindow pageWindow = new DigitalPageWindow(Codeplug.Systems); - pageWindow.Owner = this; - pageWindow.Title = "Inhibit Subscriber"; + PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); - if (pageWindow.ShowDialog() == true) - { - PeerSystem handler = fneSystemManager.GetFneSystem(pageWindow.RadioSystem.Name); - IOSP_EXT_FNCT extFunc = new IOSP_EXT_FNCT((ushort)ExtendedFunction.INHIBIT, P25Defines.WUID_FNE, uint.Parse(pageWindow.DstId)); + channel.chunkedPcm = AudioConverter.SplitToChunks(chunk); - RemoteCallData callData = new RemoteCallData - { - SrcId = uint.Parse(pageWindow.RadioSystem.Rid), - DstId = uint.Parse(pageWindow.DstId), - LCO = P25Defines.TSBK_IOSP_EXT_FNCT - }; + foreach (byte[] smallchunk in channel.chunkedPcm) + { + if (smallchunk.Length == 320) + P25EncodeAudioFrame(smallchunk, handler, channel, cpgChannel, system); + } - byte[] tsbk = new byte[P25Defines.P25_TSBK_LENGTH_BYTES]; + DateTime nextPacketTime = startTime.AddMilliseconds((i + 1) * 100); + TimeSpan waitTime = nextPacketTime - DateTime.UtcNow; - extFunc.Encode(ref tsbk); - - handler.SendP25TSBK(callData, tsbk); - } - } - - /// - /// - /// - /// - /// - private void UninhibitRID_Click(object sender, RoutedEventArgs e) - { - DigitalPageWindow pageWindow = new DigitalPageWindow(Codeplug.Systems); - pageWindow.Owner = this; - pageWindow.Title = "Uninhibit Subscriber"; - - if (pageWindow.ShowDialog() == true) - { - PeerSystem handler = fneSystemManager.GetFneSystem(pageWindow.RadioSystem.Name); - IOSP_EXT_FNCT extFunc = new IOSP_EXT_FNCT((ushort)ExtendedFunction.UNINHIBIT, P25Defines.WUID_FNE, uint.Parse(pageWindow.DstId)); - - RemoteCallData callData = new RemoteCallData - { - SrcId = uint.Parse(pageWindow.RadioSystem.Rid), - DstId = uint.Parse(pageWindow.DstId), - LCO = P25Defines.TSBK_IOSP_EXT_FNCT - }; - - byte[] tsbk = new byte[P25Defines.P25_TSBK_LENGTH_BYTES]; - - extFunc.Encode(ref tsbk); - - handler.SendP25TSBK(callData, tsbk); - } - } - - /// - /// - /// - /// - /// - private async void ManualPage_Click(object sender, RoutedEventArgs e) - { - QuickCallPage pageWindow = new QuickCallPage(); - pageWindow.Owner = this; - - if (pageWindow.ShowDialog() == true) - { - foreach (ChannelBox channel in selectedChannelsManager.GetSelectedChannels()) - { - Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); - Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); - PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); - - if (channel.PageState) - { - ToneGenerator generator = new ToneGenerator(); - - double toneADuration = 1.0; - double toneBDuration = 3.0; - - byte[] toneA = generator.GenerateTone(Double.Parse(pageWindow.ToneA), toneADuration); - byte[] toneB = generator.GenerateTone(Double.Parse(pageWindow.ToneB), toneBDuration); - - byte[] combinedAudio = new byte[toneA.Length + toneB.Length]; - Buffer.BlockCopy(toneA, 0, combinedAudio, 0, toneA.Length); - Buffer.BlockCopy(toneB, 0, combinedAudio, toneA.Length, toneB.Length); - - int chunkSize = 320; - - int totalChunks = (combinedAudio.Length + chunkSize - 1) / chunkSize; - - Task.Run(() => - { - //_waveProvider.ClearBuffer(); - audioManager.AddTalkgroupStream(cpgChannel.Tgid, combinedAudio); - }); - - await Task.Run(() => - { - for (int i = 0; i < totalChunks; i++) - { - int offset = i * chunkSize; - int size = Math.Min(chunkSize, combinedAudio.Length - offset); - - byte[] chunk = new byte[chunkSize]; - Buffer.BlockCopy(combinedAudio, offset, chunk, 0, size); - - if (chunk.Length == 320) - P25EncodeAudioFrame(chunk, handler, channel, cpgChannel, system); - } - }); - - double totalDurationMs = (toneADuration + toneBDuration) * 1000 + 750; - await Task.Delay((int)totalDurationMs + 4000); - - handler.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), false); - - Dispatcher.Invoke(() => - { - //channel.PageState = false; // TODO: Investigate - channel.PageSelectButton.Background = ChannelBox.GRAY_GRADIENT; - }); - } - } - } - } - - /// - /// - /// - /// - private void SendAlertTone(AlertTone e) - { - Task.Run(() => SendAlertTone(e.AlertFilePath)); - } - - /// - /// - /// - /// - /// - private void SendAlertTone(string filePath, bool forHold = false) - { - if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath)) - { - try - { - foreach (ChannelBox channel in selectedChannelsManager.GetSelectedChannels()) - { - if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG) - continue; - - Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); - Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); - PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); - - if (channel.PageState || (forHold && channel.HoldState)) - { - byte[] pcmData; - - Task.Run(async () => { - using (var waveReader = new WaveFileReader(filePath)) - { - if (waveReader.WaveFormat.Encoding != WaveFormatEncoding.Pcm || - waveReader.WaveFormat.SampleRate != 8000 || - waveReader.WaveFormat.BitsPerSample != 16 || - waveReader.WaveFormat.Channels != 1) - { - MessageBox.Show("The alert tone must be PCM 16-bit, Mono, 8000Hz format.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); - return; - } - - using (MemoryStream ms = new MemoryStream()) - { - waveReader.CopyTo(ms); - pcmData = ms.ToArray(); - } - } - - int chunkSize = 1600; - int totalChunks = (pcmData.Length + chunkSize - 1) / chunkSize; - - if (pcmData.Length % chunkSize != 0) - { - byte[] paddedData = new byte[totalChunks * chunkSize]; - Buffer.BlockCopy(pcmData, 0, paddedData, 0, pcmData.Length); - pcmData = paddedData; - } - - Task.Run(() => - { - audioManager.AddTalkgroupStream(cpgChannel.Tgid, pcmData); - }); - - DateTime startTime = DateTime.UtcNow; - - for (int i = 0; i < totalChunks; i++) - { - int offset = i * chunkSize; - byte[] chunk = new byte[chunkSize]; - Buffer.BlockCopy(pcmData, offset, chunk, 0, chunkSize); - - PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); - - channel.chunkedPcm = AudioConverter.SplitToChunks(chunk); - - foreach (byte[] smallchunk in channel.chunkedPcm) - { - if (smallchunk.Length == 320) - P25EncodeAudioFrame(smallchunk, handler, channel, cpgChannel, system); - } - - DateTime nextPacketTime = startTime.AddMilliseconds((i + 1) * 100); - TimeSpan waitTime = nextPacketTime - DateTime.UtcNow; - - if (waitTime.TotalMilliseconds > 0) - await Task.Delay(waitTime); - } + if (waitTime.TotalMilliseconds > 0) + await Task.Delay(waitTime); + } double totalDurationMs = ((double)pcmData.Length / 16000) + 250; await Task.Delay((int)totalDurationMs + 3000); @@ -838,30 +551,7 @@ namespace dvmconsole } } else - { MessageBox.Show("Alert file not set or file not found.", "Alert", MessageBoxButton.OK, MessageBoxImage.Warning); - } - } - - /// - /// - /// - /// - /// - private void SelectWidgets_Click(object sender, RoutedEventArgs e) - { - WidgetSelectionWindow widgetSelectionWindow = new WidgetSelectionWindow(); - widgetSelectionWindow.Owner = this; - if (widgetSelectionWindow.ShowDialog() == true) - { - settingsManager.ShowSystemStatus = widgetSelectionWindow.ShowSystemStatus; - settingsManager.ShowChannels = widgetSelectionWindow.ShowChannels; - settingsManager.ShowAlertTones = widgetSelectionWindow.ShowAlertTones; - - GenerateChannelWidgets(); - if (!noSaveSettingsOnClose) - settingsManager.SaveSettings(); - } } /// @@ -899,32 +589,32 @@ namespace dvmconsole /// /// /// - /// - /// - private void ChannelBox_HoldChannelButtonClicked(object sender, ChannelBox e) + private void UpdateEditModeForWidgets() { - if (e.SystemName == PLAYBACKSYS || e.ChannelName == PLAYBACKCHNAME || e.DstId == PLAYBACKTG) - return; + foreach (var child in channelsCanvas.Children) + { + if (child is AlertTone alertTone) + alertTone.IsEditMode = isEditMode; + + if (child is ChannelBox channelBox) + channelBox.IsEditMode = isEditMode; + } } /// /// /// - /// - /// - private void ChannelBox_PageButtonClicked(object sender, ChannelBox e) + private void UpdateBackground() { - if (e.SystemName == PLAYBACKSYS || e.ChannelName == PLAYBACKCHNAME || e.DstId == PLAYBACKTG) - return; - - Codeplug.System system = Codeplug.GetSystemForChannel(e.ChannelName); - Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(e.ChannelName); - PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); - - if (e.PageState) - handler.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), true); + BitmapImage bg = new BitmapImage(); + bg.BeginInit(); + if (settingsManager.DarkMode) + bg.UriSource = new Uri($"{URI_RESOURCE_PATH}/Assets/bg_main_hd_dark.png"); else - handler.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), false); + bg.UriSource = new Uri($"{URI_RESOURCE_PATH}/Assets/bg_main_hd_light.png"); + bg.EndInit(); + + channelsCanvasBg.ImageSource = bg; } /// @@ -932,67 +622,144 @@ namespace dvmconsole /// /// /// - private void ChannelBox_PTTButtonClicked(object sender, ChannelBox e) + private async void OnHoldTimerElapsed(object sender, ElapsedEventArgs e) { - if (e.SystemName == PLAYBACKSYS || e.ChannelName == PLAYBACKCHNAME || e.DstId == PLAYBACKTG) - return; - - Codeplug.System system = Codeplug.GetSystemForChannel(e.ChannelName); - Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(e.ChannelName); - PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); + foreach (ChannelBox channel in selectedChannelsManager.GetSelectedChannels()) + { + if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG) + continue; - if (!e.IsSelected) - return; + Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); + Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); + PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); - FneUtils.Memset(e.mi, 0x00, P25Defines.P25_MI_LENGTH); + if (channel.HoldState && !channel.IsReceiving && !channel.PttState && !channel.PageState) + { + handler.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), true); + await Task.Delay(1000); - uint srcId = uint.Parse(system.Rid); - uint dstId = uint.Parse(cpgChannel.Tgid); + SendAlertTone(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Audio/hold.wav"), true); + } + } + } - if (e.PttState) + /// + /// + /// + /// + protected override void OnClosing(System.ComponentModel.CancelEventArgs e) + { + if (!noSaveSettingsOnClose) { - e.txStreamId = handler.NewStreamId(); - handler.SendP25TDU(srcId, dstId, true); + if (WindowState == WindowState.Maximized) + settingsManager.Maximized = true; + + settingsManager.SaveSettings(); } - else - handler.SendP25TDU(srcId, dstId, false); + + base.OnClosing(e); + Application.Current.Shutdown(); } + /** NAudio Events */ + /// /// /// /// /// - private void ChannelBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + private void WaveIn_RecordingStopped(object sender, EventArgs e) { - if (!isEditMode || !(sender is UIElement element)) return; + /* stub */ + } - draggedElement = element; - startPoint = e.GetPosition(channelsCanvas); - offsetX = startPoint.X - Canvas.GetLeft(draggedElement); - offsetY = startPoint.Y - Canvas.GetTop(draggedElement); - isDragging = true; + /// + /// + /// + /// + /// + private void WaveIn_DataAvailable(object sender, WaveInEventArgs e) + { + bool isAnyTgOn = false; - Cursor = Cursors.ScrollAll; + foreach (ChannelBox channel in selectedChannelsManager.GetSelectedChannels()) + { + if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG) + continue; - element.CaptureMouse(); + Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); + Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); + + + PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); + + if (channel.IsSelected && channel.PttState) + { + isAnyTgOn = true; + + int samples = 320; + + Task.Run(() => + { + channel.chunkedPcm = AudioConverter.SplitToChunks(e.Buffer); + + foreach (byte[] chunk in channel.chunkedPcm) + { + if (chunk.Length == samples) + P25EncodeAudioFrame(chunk, handler, channel, cpgChannel, system); + else + Trace.WriteLine("bad sample length: " + chunk.Length); + } + }); + } + } + + if (playbackChannelBox != null && isAnyTgOn && playbackChannelBox.IsSelected) + audioManager.AddTalkgroupStream(PLAYBACKTG, e.Buffer); } + /** WPF Window Events */ + /// /// /// /// /// - private void ChannelBox_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) + /// + private void MainWindow_SizeChanged(object sender, SizeChangedEventArgs e) { - if (!isEditMode || !isDragging || draggedElement == null) + const double widthOffset = 16; + const double heightOffset = 115; + + if (!windowLoaded) return; - Cursor = Cursors.Arrow; + if (ActualWidth > channelsCanvas.ActualWidth) + { + channelsCanvas.Width = ActualWidth; + canvasScrollViewer.Width = ActualWidth; + } + else + canvasScrollViewer.Width = Width - widthOffset; - isDragging = false; - draggedElement.ReleaseMouseCapture(); - draggedElement = null; + if (ActualHeight > channelsCanvas.ActualHeight) + { + channelsCanvas.Height = ActualHeight; + canvasScrollViewer.Height = ActualHeight; + } + else + canvasScrollViewer.Height = Height - heightOffset; + + if (WindowState == WindowState.Maximized) + ResizeCanvasToWindow_Click(sender, e); + else + settingsManager.Maximized = false; + + settingsManager.CanvasWidth = channelsCanvas.ActualWidth; + settingsManager.CanvasHeight = channelsCanvas.ActualHeight; + + settingsManager.WindowWidth = ActualWidth; + settingsManager.WindowHeight = ActualHeight; } /// @@ -1000,28 +767,43 @@ namespace dvmconsole /// /// /// - private void ChannelBox_MouseMove(object sender, MouseEventArgs e) + private void MainWindow_Loaded(object sender, RoutedEventArgs e) { - if (!isEditMode || !isDragging || draggedElement == null) - return; + const double widthOffset = 16; + const double heightOffset = 115; - Point currentPosition = e.GetPosition(channelsCanvas); + if (!string.IsNullOrEmpty(settingsManager.LastCodeplugPath) && File.Exists(settingsManager.LastCodeplugPath)) + LoadCodeplug(settingsManager.LastCodeplugPath); + else + GenerateChannelWidgets(); - // Calculate the new position with snapping to the grid - double newLeft = Math.Round((currentPosition.X - offsetX) / GridSize) * GridSize; - double newTop = Math.Round((currentPosition.Y - offsetY) / GridSize) * GridSize; + menuDarkMode.IsChecked = settingsManager.DarkMode; + UpdateBackground(); - // Ensure the box stays within canvas bounds - newLeft = Math.Max(0, Math.Min(newLeft, channelsCanvas.ActualWidth - draggedElement.RenderSize.Width)); - newTop = Math.Max(0, Math.Min(newTop, channelsCanvas.ActualHeight - draggedElement.RenderSize.Height)); + if (settingsManager.Maximized) + { + windowLoaded = true; + WindowState = WindowState.Maximized; + ResizeCanvasToWindow_Click(sender, e); + } + else + { + Width = settingsManager.WindowWidth; + channelsCanvas.Width = settingsManager.CanvasWidth; + if (settingsManager.CanvasWidth > settingsManager.WindowWidth) + canvasScrollViewer.Width = Width - widthOffset; + else + canvasScrollViewer.Width = Width; - // Apply snapped position - Canvas.SetLeft(draggedElement, newLeft); - Canvas.SetTop(draggedElement, newTop); + Height = settingsManager.WindowHeight; + channelsCanvas.Height = settingsManager.CanvasHeight; + if (settingsManager.CanvasHeight > settingsManager.WindowHeight) + canvasScrollViewer.Height = Height - heightOffset; + else + canvasScrollViewer.Height = Height; - // Save the new position if it's a ChannelBox - if (draggedElement is ChannelBox channelBox) - settingsManager.UpdateChannelPosition(channelBox.ChannelName, newLeft, newTop); + windowLoaded = true; + } } /// @@ -1029,26 +811,45 @@ namespace dvmconsole /// /// /// - private void SystemStatusBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) => ChannelBox_MouseLeftButtonDown(sender, e); + private void OpenCodeplug_Click(object sender, RoutedEventArgs e) + { + OpenFileDialog openFileDialog = new OpenFileDialog + { + Filter = "Codeplug Files (*.yml)|*.yml|All Files (*.*)|*.*", + Title = "Open Codeplug" + }; + + if (openFileDialog.ShowDialog() == true) + { + LoadCodeplug(openFileDialog.FileName); + + settingsManager.LastCodeplugPath = openFileDialog.FileName; + noSaveSettingsOnClose = false; + settingsManager.SaveSettings(); + } + } /// /// /// /// /// - private void SystemStatusBox_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) + private void Exit_Click(object sender, RoutedEventArgs e) { - if (!isEditMode) - return; + Application.Current.Shutdown(); + } - if (sender is SystemStatusBox systemStatusBox) - { - double x = Canvas.GetLeft(systemStatusBox); - double y = Canvas.GetTop(systemStatusBox); - settingsManager.SystemStatusPositions[systemStatusBox.SystemName] = new ChannelPosition { X = x, Y = y }; + /// + /// + /// + /// + /// + private void AudioSettings_Click(object sender, RoutedEventArgs e) + { + List channels = Codeplug?.Zones.SelectMany(z => z.Channels).ToList() ?? new List(); - ChannelBox_MouseLeftButtonUp(sender, e); - } + AudioSettingsWindow audioSettingsWindow = new AudioSettingsWindow(settingsManager, audioManager, channels); + audioSettingsWindow.ShowDialog(); } /// @@ -1056,7 +857,16 @@ namespace dvmconsole /// /// /// - private void SystemStatusBox_MouseMove(object sender, MouseEventArgs e) => ChannelBox_MouseMove(sender, e); + private void ResetSettings_Click(object sender, RoutedEventArgs e) + { + var confirmResult = MessageBox.Show("Are you sure to wish to reset console settings?", "Reset Settings", MessageBoxButton.YesNo, MessageBoxImage.Question); + if (confirmResult == MessageBoxResult.Yes) + { + MessageBox.Show("Settings will be reset after console restart.", "Reset Settings", MessageBoxButton.OK, MessageBoxImage.Information); + noSaveSettingsOnClose = true; + settingsManager.Reset(); + } + } /// /// @@ -1074,16 +884,15 @@ namespace dvmconsole /// /// /// - private void UpdateEditModeForWidgets() + /// + /// + private void ToggleDarkMode_Click(object sender, RoutedEventArgs e) { - foreach (var child in channelsCanvas.Children) - { - if (child is AlertTone alertTone) - alertTone.IsEditMode = isEditMode; + if (!windowLoaded) + return; - if (child is ChannelBox channelBox) - channelBox.IsEditMode = isEditMode; - } + settingsManager.DarkMode = menuDarkMode.IsChecked; + UpdateBackground(); } /// @@ -1118,6 +927,27 @@ namespace dvmconsole settingsManager.WindowHeight = ActualHeight; } + /// + /// + /// + /// + /// + private void SelectWidgets_Click(object sender, RoutedEventArgs e) + { + WidgetSelectionWindow widgetSelectionWindow = new WidgetSelectionWindow(); + widgetSelectionWindow.Owner = this; + if (widgetSelectionWindow.ShowDialog() == true) + { + settingsManager.ShowSystemStatus = widgetSelectionWindow.ShowSystemStatus; + settingsManager.ShowChannels = widgetSelectionWindow.ShowChannels; + settingsManager.ShowAlertTones = widgetSelectionWindow.ShowAlertTones; + + GenerateChannelWidgets(); + if (!noSaveSettingsOnClose) + settingsManager.SaveSettings(); + } + } + /// /// /// @@ -1161,15 +991,29 @@ namespace dvmconsole /// /// /// - private void AlertTone_MouseRightButtonUp(object sender, MouseButtonEventArgs e) + private void PageRID_Click(object sender, RoutedEventArgs e) { - if (!isEditMode) return; + DigitalPageWindow pageWindow = new DigitalPageWindow(Codeplug.Systems); + pageWindow.Owner = this; + pageWindow.Title = "Page Subscriber"; - if (sender is AlertTone alertTone) + if (pageWindow.ShowDialog() == true) { - double x = Canvas.GetLeft(alertTone); - double y = Canvas.GetTop(alertTone); - settingsManager.UpdateAlertTonePosition(alertTone.AlertFilePath, x, y); + PeerSystem handler = fneSystemManager.GetFneSystem(pageWindow.RadioSystem.Name); + IOSP_CALL_ALRT callAlert = new IOSP_CALL_ALRT(uint.Parse(pageWindow.DstId), uint.Parse(pageWindow.RadioSystem.Rid)); + + RemoteCallData callData = new RemoteCallData + { + SrcId = uint.Parse(pageWindow.RadioSystem.Rid), + DstId = uint.Parse(pageWindow.DstId), + LCO = P25Defines.TSBK_IOSP_CALL_ALRT + }; + + byte[] tsbk = new byte[P25Defines.P25_TSBK_LENGTH_BYTES]; + + callAlert.Encode(ref tsbk); + + handler.SendP25TSBK(callData, tsbk); } } @@ -1178,41 +1022,228 @@ namespace dvmconsole /// /// /// - /// - private void MainWindow_SizeChanged(object sender, SizeChangedEventArgs e) + private void RadioCheckRID_Click(object sender, RoutedEventArgs e) { - const double widthOffset = 16; - const double heightOffset = 115; + DigitalPageWindow pageWindow = new DigitalPageWindow(Codeplug.Systems); + pageWindow.Owner = this; + pageWindow.Title = "Radio Check Subscriber"; - if (!windowLoaded) - return; + if (pageWindow.ShowDialog() == true) + { + PeerSystem handler = fneSystemManager.GetFneSystem(pageWindow.RadioSystem.Name); + IOSP_EXT_FNCT extFunc = new IOSP_EXT_FNCT((ushort)ExtendedFunction.CHECK, uint.Parse(pageWindow.RadioSystem.Rid), uint.Parse(pageWindow.DstId)); - if (ActualWidth > channelsCanvas.ActualWidth) + RemoteCallData callData = new RemoteCallData + { + SrcId = uint.Parse(pageWindow.RadioSystem.Rid), + DstId = uint.Parse(pageWindow.DstId), + LCO = P25Defines.TSBK_IOSP_EXT_FNCT + }; + + byte[] tsbk = new byte[P25Defines.P25_TSBK_LENGTH_BYTES]; + + extFunc.Encode(ref tsbk); + + handler.SendP25TSBK(callData, tsbk); + } + } + + /// + /// + /// + /// + /// + private void InhibitRID_Click(object sender, RoutedEventArgs e) + { + DigitalPageWindow pageWindow = new DigitalPageWindow(Codeplug.Systems); + pageWindow.Owner = this; + pageWindow.Title = "Inhibit Subscriber"; + + if (pageWindow.ShowDialog() == true) { - channelsCanvas.Width = ActualWidth; - canvasScrollViewer.Width = ActualWidth; + PeerSystem handler = fneSystemManager.GetFneSystem(pageWindow.RadioSystem.Name); + IOSP_EXT_FNCT extFunc = new IOSP_EXT_FNCT((ushort)ExtendedFunction.INHIBIT, P25Defines.WUID_FNE, uint.Parse(pageWindow.DstId)); + + RemoteCallData callData = new RemoteCallData + { + SrcId = uint.Parse(pageWindow.RadioSystem.Rid), + DstId = uint.Parse(pageWindow.DstId), + LCO = P25Defines.TSBK_IOSP_EXT_FNCT + }; + + byte[] tsbk = new byte[P25Defines.P25_TSBK_LENGTH_BYTES]; + + extFunc.Encode(ref tsbk); + + handler.SendP25TSBK(callData, tsbk); } - else - canvasScrollViewer.Width = Width - widthOffset; + } - if (ActualHeight > channelsCanvas.ActualHeight) + /// + /// + /// + /// + /// + private void UninhibitRID_Click(object sender, RoutedEventArgs e) + { + DigitalPageWindow pageWindow = new DigitalPageWindow(Codeplug.Systems); + pageWindow.Owner = this; + pageWindow.Title = "Uninhibit Subscriber"; + + if (pageWindow.ShowDialog() == true) { - channelsCanvas.Height = ActualHeight; - canvasScrollViewer.Height = ActualHeight; + PeerSystem handler = fneSystemManager.GetFneSystem(pageWindow.RadioSystem.Name); + IOSP_EXT_FNCT extFunc = new IOSP_EXT_FNCT((ushort)ExtendedFunction.UNINHIBIT, P25Defines.WUID_FNE, uint.Parse(pageWindow.DstId)); + + RemoteCallData callData = new RemoteCallData + { + SrcId = uint.Parse(pageWindow.RadioSystem.Rid), + DstId = uint.Parse(pageWindow.DstId), + LCO = P25Defines.TSBK_IOSP_EXT_FNCT + }; + + byte[] tsbk = new byte[P25Defines.P25_TSBK_LENGTH_BYTES]; + + extFunc.Encode(ref tsbk); + + handler.SendP25TSBK(callData, tsbk); } - else - canvasScrollViewer.Height = Height - heightOffset; + } - if (WindowState == WindowState.Maximized) - ResizeCanvasToWindow_Click(sender, e); + /// + /// + /// + /// + /// + private async void ManualPage_Click(object sender, RoutedEventArgs e) + { + QuickCallPage pageWindow = new QuickCallPage(); + pageWindow.Owner = this; + + if (pageWindow.ShowDialog() == true) + { + foreach (ChannelBox channel in selectedChannelsManager.GetSelectedChannels()) + { + Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); + Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); + PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); + + if (channel.PageState) + { + ToneGenerator generator = new ToneGenerator(); + + double toneADuration = 1.0; + double toneBDuration = 3.0; + + byte[] toneA = generator.GenerateTone(Double.Parse(pageWindow.ToneA), toneADuration); + byte[] toneB = generator.GenerateTone(Double.Parse(pageWindow.ToneB), toneBDuration); + + byte[] combinedAudio = new byte[toneA.Length + toneB.Length]; + Buffer.BlockCopy(toneA, 0, combinedAudio, 0, toneA.Length); + Buffer.BlockCopy(toneB, 0, combinedAudio, toneA.Length, toneB.Length); + + int chunkSize = 320; + + int totalChunks = (combinedAudio.Length + chunkSize - 1) / chunkSize; + + Task.Run(() => + { + //_waveProvider.ClearBuffer(); + audioManager.AddTalkgroupStream(cpgChannel.Tgid, combinedAudio); + }); + + await Task.Run(() => + { + for (int i = 0; i < totalChunks; i++) + { + int offset = i * chunkSize; + int size = Math.Min(chunkSize, combinedAudio.Length - offset); + + byte[] chunk = new byte[chunkSize]; + Buffer.BlockCopy(combinedAudio, offset, chunk, 0, size); + + if (chunk.Length == 320) + P25EncodeAudioFrame(chunk, handler, channel, cpgChannel, system); + } + }); + + double totalDurationMs = (toneADuration + toneBDuration) * 1000 + 750; + await Task.Delay((int)totalDurationMs + 4000); + + handler.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), false); + + Dispatcher.Invoke(() => + { + //channel.PageState = false; // TODO: Investigate + channel.PageSelectButton.Background = ChannelBox.GRAY_GRADIENT; + }); + } + } + } + } + + /** Widget Controls */ + + /// + /// + /// + /// + /// + private void ChannelBox_HoldChannelButtonClicked(object sender, ChannelBox e) + { + if (e.SystemName == PLAYBACKSYS || e.ChannelName == PLAYBACKCHNAME || e.DstId == PLAYBACKTG) + return; + } + + /// + /// + /// + /// + /// + private void ChannelBox_PageButtonClicked(object sender, ChannelBox e) + { + if (e.SystemName == PLAYBACKSYS || e.ChannelName == PLAYBACKCHNAME || e.DstId == PLAYBACKTG) + return; + + Codeplug.System system = Codeplug.GetSystemForChannel(e.ChannelName); + Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(e.ChannelName); + PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); + + if (e.PageState) + handler.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), true); else - settingsManager.Maximized = false; + handler.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), false); + } + + /// + /// + /// + /// + /// + private void ChannelBox_PTTButtonClicked(object sender, ChannelBox e) + { + if (e.SystemName == PLAYBACKSYS || e.ChannelName == PLAYBACKCHNAME || e.DstId == PLAYBACKTG) + return; + + Codeplug.System system = Codeplug.GetSystemForChannel(e.ChannelName); + Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(e.ChannelName); + PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); + + if (!e.IsSelected) + return; - settingsManager.CanvasWidth = channelsCanvas.ActualWidth; - settingsManager.CanvasHeight = channelsCanvas.ActualHeight; + FneUtils.Memset(e.mi, 0x00, P25Defines.P25_MI_LENGTH); - settingsManager.WindowWidth = ActualWidth; - settingsManager.WindowHeight = ActualHeight; + uint srcId = uint.Parse(system.Rid); + uint dstId = uint.Parse(cpgChannel.Tgid); + + if (e.PttState) + { + e.TxStreamId = handler.NewStreamId(); + handler.SendP25TDU(srcId, dstId, true); + } + else + handler.SendP25TDU(srcId, dstId, false); } /// @@ -1220,41 +1251,19 @@ namespace dvmconsole /// /// /// - private void MainWindow_Loaded(object sender, RoutedEventArgs e) + private void ChannelBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { - const double widthOffset = 16; - const double heightOffset = 115; - - if (!string.IsNullOrEmpty(settingsManager.LastCodeplugPath) && File.Exists(settingsManager.LastCodeplugPath)) - LoadCodeplug(settingsManager.LastCodeplugPath); - else - GenerateChannelWidgets(); + if (!isEditMode || !(sender is UIElement element)) return; - if (settingsManager.Maximized) - { - windowLoaded = true; - WindowState = WindowState.Maximized; - //Application.Current.MainWindow.WindowState = WindowState.Maximized; - ResizeCanvasToWindow_Click(sender, e); - } - else - { - Width = settingsManager.WindowWidth; - channelsCanvas.Width = settingsManager.CanvasWidth; - if (settingsManager.CanvasWidth > settingsManager.WindowWidth) - canvasScrollViewer.Width = Width - widthOffset; - else - canvasScrollViewer.Width = Width; + draggedElement = element; + startPoint = e.GetPosition(channelsCanvas); + offsetX = startPoint.X - Canvas.GetLeft(draggedElement); + offsetY = startPoint.Y - Canvas.GetTop(draggedElement); + isDragging = true; - Height = settingsManager.WindowHeight; - channelsCanvas.Height = settingsManager.CanvasHeight; - if (settingsManager.CanvasHeight > settingsManager.WindowHeight) - canvasScrollViewer.Height = Height - heightOffset; - else - canvasScrollViewer.Height = Height; + Cursor = Cursors.ScrollAll; - windowLoaded = true; - } + element.CaptureMouse(); } /// @@ -1262,43 +1271,45 @@ namespace dvmconsole /// /// /// - private async void OnHoldTimerElapsed(object sender, ElapsedEventArgs e) + private void ChannelBox_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { - foreach (ChannelBox channel in selectedChannelsManager.GetSelectedChannels()) - { - if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG) - continue; - - Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); - Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); - PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); + if (!isEditMode || !isDragging || draggedElement == null) + return; - if (channel.HoldState && !channel.IsReceiving && !channel.PttState && !channel.PageState) - { - handler.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), true); - await Task.Delay(1000); + Cursor = Cursors.Arrow; - SendAlertTone(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Audio/hold.wav"), true); - } - } + isDragging = false; + draggedElement.ReleaseMouseCapture(); + draggedElement = null; } /// /// /// + /// /// - protected override void OnClosing(System.ComponentModel.CancelEventArgs e) + private void ChannelBox_MouseMove(object sender, MouseEventArgs e) { - if (!noSaveSettingsOnClose) - { - if (WindowState == WindowState.Maximized) - settingsManager.Maximized = true; + if (!isEditMode || !isDragging || draggedElement == null) + return; - settingsManager.SaveSettings(); - } + Point currentPosition = e.GetPosition(channelsCanvas); - base.OnClosing(e); - Application.Current.Shutdown(); + // Calculate the new position with snapping to the grid + double newLeft = Math.Round((currentPosition.X - offsetX) / GridSize) * GridSize; + double newTop = Math.Round((currentPosition.Y - offsetY) / GridSize) * GridSize; + + // Ensure the box stays within canvas bounds + newLeft = Math.Max(0, Math.Min(newLeft, channelsCanvas.ActualWidth - draggedElement.RenderSize.Width)); + newTop = Math.Max(0, Math.Min(newTop, channelsCanvas.ActualHeight - draggedElement.RenderSize.Height)); + + // Apply snapped position + Canvas.SetLeft(draggedElement, newLeft); + Canvas.SetTop(draggedElement, newTop); + + // Save the new position if it's a ChannelBox + if (draggedElement is ChannelBox channelBox) + settingsManager.UpdateChannelPosition(channelBox.ChannelName, newLeft, newTop); } /// @@ -1306,13 +1317,26 @@ namespace dvmconsole /// /// /// - private void ClearEmergency_Click(object sender, RoutedEventArgs e) + private void SystemStatusBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) => ChannelBox_MouseLeftButtonDown(sender, e); + + /// + /// + /// + /// + /// + private void SystemStatusBox_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { - emergencyAlertPlayback.Stop(); - flashingManager.Stop(); + if (!isEditMode) + return; - foreach (ChannelBox channel in selectedChannelsManager.GetSelectedChannels()) - channel.Emergency = false; + if (sender is SystemStatusBox systemStatusBox) + { + double x = Canvas.GetLeft(systemStatusBox); + double y = Canvas.GetTop(systemStatusBox); + settingsManager.SystemStatusPositions[systemStatusBox.SystemName] = new ChannelPosition { X = x, Y = y }; + + ChannelBox_MouseLeftButtonUp(sender, e); + } } /// @@ -1320,37 +1344,39 @@ namespace dvmconsole /// /// /// - private void btnAlert1_Click(object sender, RoutedEventArgs e) - { - Dispatcher.Invoke(() => { - SendAlertTone(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Audio/alert1.wav")); - }); - } + private void SystemStatusBox_MouseMove(object sender, MouseEventArgs e) => ChannelBox_MouseMove(sender, e); /// /// /// /// /// - private void btnAlert2_Click(object sender, RoutedEventArgs e) + private void AlertTone_MouseRightButtonUp(object sender, MouseButtonEventArgs e) { - Dispatcher.Invoke(() => + if (!isEditMode) return; + + if (sender is AlertTone alertTone) { - SendAlertTone(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Audio/alert2.wav")); - }); + double x = Canvas.GetLeft(alertTone); + double y = Canvas.GetTop(alertTone); + settingsManager.UpdateAlertTonePosition(alertTone.AlertFilePath, x, y); + } } + /** WPF Ribbon Controls */ + /// /// /// /// /// - private void btnAlert3_Click(object sender, RoutedEventArgs e) + private void ClearEmergency_Click(object sender, RoutedEventArgs e) { - Dispatcher.Invoke(() => - { - SendAlertTone(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Audio/alert3.wav")); - }); + emergencyAlertPlayback.Stop(); + flashingManager.Stop(); + + foreach (ChannelBox channel in selectedChannelsManager.GetSelectedChannels()) + channel.Emergency = false; } /// @@ -1374,7 +1400,7 @@ namespace dvmconsole Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); PeerSystem handler = fneSystemManager.GetFneSystem(system.Name); - channel.txStreamId = handler.NewStreamId(); + channel.TxStreamId = handler.NewStreamId(); if (globalPttState) { @@ -1399,6 +1425,44 @@ namespace dvmconsole } } + /// + /// + /// + /// + /// + private void btnAlert1_Click(object sender, RoutedEventArgs e) + { + Dispatcher.Invoke(() => { + SendAlertTone(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Audio/alert1.wav")); + }); + } + + /// + /// + /// + /// + /// + private void btnAlert2_Click(object sender, RoutedEventArgs e) + { + Dispatcher.Invoke(() => + { + SendAlertTone(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Audio/alert2.wav")); + }); + } + + /// + /// + /// + /// + /// + private void btnAlert3_Click(object sender, RoutedEventArgs e) + { + Dispatcher.Invoke(() => + { + SendAlertTone(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Audio/alert3.wav")); + }); + } + /// /// /// @@ -1425,6 +1489,31 @@ namespace dvmconsole } } + /// + /// + /// + /// + /// + private void KeyStatus_Click(object sender, RoutedEventArgs e) + { + KeyStatusWindow keyStatus = new KeyStatusWindow(Codeplug, this); + keyStatus.Owner = this; + keyStatus.Show(); + } + + /// + /// + /// + /// + /// + private void CallHist_Click(object sender, RoutedEventArgs e) + { + callHistoryWindow.Owner = this; + callHistoryWindow.Show(); + } + + /** fnecore Hooks / Helpers */ + /// /// Helper to encode and transmit PCM audio as P25 IMBE frames. /// @@ -1511,9 +1600,7 @@ namespace dvmconsole Random random = new Random(); for (int i = 0; i < P25Defines.P25_MI_LENGTH; i++) - { channel.mi[i] = (byte)random.Next(0x00, 0x100); - } } channel.Crypter.Prepare(cpgChannel.GetAlgoId(), cpgChannel.GetKeyId(), channel.mi); @@ -1618,7 +1705,7 @@ namespace dvmconsole handler.CreateNewP25MessageHdr((byte)P25DUID.LDU1, callData, ref payload, cpgChannel.GetAlgoId(), cpgChannel.GetKeyId(), channel.mi); handler.CreateP25LDU1Message(channel.netLDU1, ref payload, srcId, dstId); - peer.SendMaster(new Tuple(Constants.NET_FUNC_PROTOCOL, Constants.NET_PROTOCOL_SUBFUNC_P25), payload, pktSeq, channel.txStreamId); + peer.SendMaster(new Tuple(Constants.NET_FUNC_PROTOCOL, Constants.NET_PROTOCOL_SUBFUNC_P25), payload, pktSeq, channel.TxStreamId); } // send P25 LDU2 @@ -1636,7 +1723,7 @@ namespace dvmconsole handler.CreateNewP25MessageHdr((byte)P25DUID.LDU2, callData, ref payload, cpgChannel.GetAlgoId(), cpgChannel.GetKeyId(), channel.mi); handler.CreateP25LDU2Message(channel.netLDU2, ref payload, new CryptoParams { AlgId = cpgChannel.GetAlgoId(), KeyId = cpgChannel.GetKeyId(), MI = channel.mi }); - peer.SendMaster(new Tuple(Constants.NET_FUNC_PROTOCOL, Constants.NET_PROTOCOL_SUBFUNC_P25), payload, pktSeq, channel.txStreamId); + peer.SendMaster(new Tuple(Constants.NET_FUNC_PROTOCOL, Constants.NET_PROTOCOL_SUBFUNC_P25), payload, pktSeq, channel.TxStreamId); } channel.p25SeqNo++; @@ -1781,18 +1868,6 @@ namespace dvmconsole } } - /// - /// - /// - /// - /// - private void KeyStatus_Click(object sender, RoutedEventArgs e) - { - KeyStatusWindow keyStatus = new KeyStatusWindow(Codeplug, this); - keyStatus.Owner = this; - keyStatus.Show(); - } - /// /// Event handler used to process incoming P25 data. /// @@ -2040,26 +2115,5 @@ namespace dvmconsole } }); } - - /// - /// - /// - /// - /// - private void CallHist_Click(object sender, RoutedEventArgs e) - { - callHistoryWindow.Owner = this; - callHistoryWindow.Show(); - } - - /// - /// - /// - /// - /// - private void Exit_Click(object sender, RoutedEventArgs e) - { - Application.Current.Shutdown(); - } } // public partial class MainWindow : Window } // namespace dvmconsole diff --git a/dvmconsole/QuickCallPage.xaml.cs b/dvmconsole/QuickCallPage.xaml.cs index 322b70f..a82b896 100644 --- a/dvmconsole/QuickCallPage.xaml.cs +++ b/dvmconsole/QuickCallPage.xaml.cs @@ -20,7 +20,13 @@ namespace dvmconsole /// public partial class QuickCallPage : Window { + /// + /// Tone A. + /// public string ToneA; + /// + /// Tone B. + /// public string ToneB; /* @@ -35,6 +41,8 @@ namespace dvmconsole InitializeComponent(); } + /** WPF Events */ + /// /// /// diff --git a/dvmconsole/SettingsManager.cs b/dvmconsole/SettingsManager.cs index fe79d60..94d749c 100644 --- a/dvmconsole/SettingsManager.cs +++ b/dvmconsole/SettingsManager.cs @@ -82,6 +82,10 @@ namespace dvmconsole /// /// /// + public bool DarkMode { get; set; } = false; + /// + /// + /// public double WindowWidth { get; set; } = MainWindow.MIN_WIDTH; /// /// @@ -128,6 +132,7 @@ namespace dvmconsole AlertTonePositions = loadedSettings.AlertTonePositions ?? new Dictionary(); ChannelOutputDevices = loadedSettings.ChannelOutputDevices ?? new Dictionary(); Maximized = loadedSettings.Maximized; + DarkMode = loadedSettings.DarkMode; WindowWidth = loadedSettings.WindowWidth; if (WindowWidth == 0) WindowWidth = MainWindow.MIN_WIDTH; diff --git a/dvmconsole/WidgetSelectionWindow.xaml.cs b/dvmconsole/WidgetSelectionWindow.xaml.cs index e09f6f4..af827aa 100644 --- a/dvmconsole/WidgetSelectionWindow.xaml.cs +++ b/dvmconsole/WidgetSelectionWindow.xaml.cs @@ -25,15 +25,15 @@ namespace dvmconsole */ /// - /// + /// Flag indicating whether or not the system status widgets appear. /// public bool ShowSystemStatus { get; private set; } = true; /// - /// + /// Flag indicating whether or not the channel widgets appear. /// public bool ShowChannels { get; private set; } = true; /// - /// + /// Flag indicating whether or not alert tone widgets appear. /// public bool ShowAlertTones { get; private set; } = true; @@ -49,6 +49,8 @@ namespace dvmconsole InitializeComponent(); } + /** WPF Events */ + /// /// ///