diff --git a/dvmconsole/Controls/ChannelBox.xaml.cs b/dvmconsole/Controls/ChannelBox.xaml.cs
index 72bc60f..904583c 100644
--- a/dvmconsole/Controls/ChannelBox.xaml.cs
+++ b/dvmconsole/Controls/ChannelBox.xaml.cs
@@ -9,6 +9,7 @@
*
* Copyright (C) 2025 Caleb, K4PHP
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
+* Copyright (C) 2025 Steven Jennison, KD8RHO
*
*/
@@ -29,6 +30,8 @@ namespace dvmconsole.Controls
///
public partial class ChannelBox : UserControl, INotifyPropertyChanged
{
+ public readonly static Border BORDER_DEFAULT;
+ public readonly static Border BORDER_GREEN;
public readonly static LinearGradientBrush GRAY_GRADIENT;
public readonly static LinearGradientBrush DARK_GRAY_GRADIENT; // Delected/Disconnected Color
public readonly static LinearGradientBrush BLUE_GRADIENT; // Selected Channel Color
@@ -265,6 +268,17 @@ namespace dvmconsole.Controls
}
}
+ private bool isPrimary = false;
+ public bool IsPrimary
+ {
+ get => isPrimary;
+ set
+ {
+ isPrimary = value;
+ UpdateBackground();
+ }
+ }
+
///
/// Current volume for this channel.
///
@@ -364,6 +378,19 @@ namespace dvmconsole.Controls
ORANGE_GRADIENT.GradientStops.Add(new GradientStop((Color)ColorConverter.ConvertFromString("#F0FFAF00"), 0.485));
ORANGE_GRADIENT.GradientStops.Add(new GradientStop((Color)ColorConverter.ConvertFromString("#F0C68700"), 0.517));
+ BORDER_DEFAULT = new Border
+ {
+ BorderBrush = new SolidColorBrush(Colors.LightGray),
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(8)
+ };
+
+ BORDER_GREEN = new Border
+ {
+ BorderBrush = new SolidColorBrush(Colors.Green),
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(8)
+ };
}
///
@@ -510,6 +537,13 @@ namespace dvmconsole.Controls
}
ControlBorder.Background = IsSelected ? BLUE_GRADIENT : DARK_GRAY_GRADIENT;
+ if (IsSelected)
+ if (IsPrimary)
+ ControlBorder.BorderBrush = BORDER_GREEN.BorderBrush;
+ else
+ ControlBorder.BorderBrush = BORDER_DEFAULT.BorderBrush;
+ else
+ ControlBorder.BorderBrush = BORDER_DEFAULT.BorderBrush;
}
///
@@ -530,6 +564,28 @@ namespace dvmconsole.Controls
///
private void ChannelBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
+ if (IsSelected)
+ {
+ // Check if either CTRL key is down, if so toggle PRIMARY state instead of deselecting
+ if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
+ {
+ // If current channel is PRIMARY, clear it, otherwise set current to primary
+ if (selectedChannelsManager.PrimaryChannel == this)
+ {
+ selectedChannelsManager.ClearPrimaryChannel();
+ IsPrimary = false;
+ }
+ else
+ {
+ selectedChannelsManager.SetPrimaryChannel(this);
+ IsPrimary = true;
+ }
+ // Shortcut return, do not run the rest of this method
+ return;
+ }
+ }
+
+
IsSelected = !IsSelected;
ControlBorder.Background = IsSelected ? BLUE_GRADIENT : DARK_GRAY_GRADIENT;
@@ -544,7 +600,7 @@ namespace dvmconsole.Controls
///
///
///
- private void PttButton_Click(object sender, RoutedEventArgs e)
+ public void PttButton_Click(object sender, RoutedEventArgs e)
{
if (!IsSelected)
return;
diff --git a/dvmconsole/MainWindow.xaml.cs b/dvmconsole/MainWindow.xaml.cs
index 8709be2..3ae9d7a 100644
--- a/dvmconsole/MainWindow.xaml.cs
+++ b/dvmconsole/MainWindow.xaml.cs
@@ -10,7 +10,7 @@
* Copyright (C) 2024-2025 Caleb, K4PHP
* Copyright (C) 2025 J. Dean
* Copyright (C) 2025 Bryan Biedenkapp, N2PLL
-* Copyright (C) 2025 Steven Jennison, KD8RHO
+* Copyright (C) 2025 Steven Jennison, KD8RHO
*
*/
@@ -46,8 +46,8 @@ namespace dvmconsole
public class ChannelPosition
{
/*
- ** Properties
- */
+ ** Properties
+ */
///
/// X
@@ -120,8 +120,8 @@ namespace dvmconsole
private bool selectAll = false;
/*
- ** Properties
- */
+ ** Properties
+ */
///
/// Codeplug
@@ -129,8 +129,8 @@ namespace dvmconsole
public Codeplug Codeplug { get; set; }
/*
- ** Methods
- */
+ ** Methods
+ */
///
/// Initializes a new instance of the class.
@@ -168,10 +168,23 @@ namespace dvmconsole
btnGlobalPtt.MouseRightButtonDown += btnGlobalPtt_MouseRightButtonDown;
selectedChannelsManager.SelectedChannelsChanged += SelectedChannelsChanged;
+ selectedChannelsManager.PrimaryChannelChanged += PrimaryChannelChanged;
SizeChanged += MainWindow_SizeChanged;
Loaded += MainWindow_Loaded;
}
+ private void PrimaryChannelChanged()
+ {
+ var primaryChannel = selectedChannelsManager.PrimaryChannel;
+ foreach (UIElement element in channelsCanvas.Children)
+ {
+ if (element is ChannelBox box)
+ {
+ box.IsPrimary = box == primaryChannel;
+ }
+ }
+ }
+
///
/// Helper to enable menu controls for Commands submenu.
///
@@ -631,119 +644,122 @@ namespace dvmconsole
{
try
{
- foreach (ChannelBox channel in selectedChannelsManager.GetSelectedChannels())
+ var channel = selectedChannelsManager.PrimaryChannel;
+ if (channel == null)
{
- if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
- continue;
+ return;
+ }
- Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
- if (system == null)
- {
- Log.WriteLine($"{channel.ChannelName} refers to an {INVALID_SYSTEM} {channel.SystemName}. {ERR_INVALID_CODEPLUG}. {ERR_SKIPPING_AUDIO}.");
- channel.IsSelected = false;
- selectedChannelsManager.RemoveSelectedChannel(channel);
- continue;
- }
+ if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
+ return;
- Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
- if (cpgChannel == null)
- {
- Log.WriteLine($"{channel.ChannelName} refers to an {INVALID_CODEPLUG_CHANNEL}. {ERR_INVALID_CODEPLUG}. {ERR_SKIPPING_AUDIO}.");
- channel.IsSelected = false;
- selectedChannelsManager.RemoveSelectedChannel(channel);
- continue;
- }
+ Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
+ if (system == null)
+ {
+ Log.WriteLine($"{channel.ChannelName} refers to an {INVALID_SYSTEM} {channel.SystemName}. {ERR_INVALID_CODEPLUG}. {ERR_SKIPPING_AUDIO}.");
+ channel.IsSelected = false;
+ selectedChannelsManager.RemoveSelectedChannel(channel);
+ return;
+ }
- PeerSystem fne = fneSystemManager.GetFneSystem(system.Name);
- if (fne == null)
- {
- Log.WriteLine($"{channel.ChannelName} has a {ERR_INVALID_FNE_REF}. {ERR_INVALID_CODEPLUG}. {ERR_SKIPPING_AUDIO}.");
- channel.IsSelected = false;
- selectedChannelsManager.RemoveSelectedChannel(channel);
- continue;
- }
+ Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
+ if (cpgChannel == null)
+ {
+ Log.WriteLine($"{channel.ChannelName} refers to an {INVALID_CODEPLUG_CHANNEL}. {ERR_INVALID_CODEPLUG}. {ERR_SKIPPING_AUDIO}.");
+ channel.IsSelected = false;
+ selectedChannelsManager.RemoveSelectedChannel(channel);
+ return;
+ }
- //
- if (channel.PageState || (forHold && channel.HoldState))
- {
- byte[] pcmData;
+ PeerSystem fne = fneSystemManager.GetFneSystem(system.Name);
+ if (fne == null)
+ {
+ Log.WriteLine($"{channel.ChannelName} has a {ERR_INVALID_FNE_REF}. {ERR_INVALID_CODEPLUG}. {ERR_SKIPPING_AUDIO}.");
+ channel.IsSelected = false;
+ selectedChannelsManager.RemoveSelectedChannel(channel);
+ return;
+ }
- 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;
- }
+ //
+ if (channel.PageState || (forHold && channel.HoldState))
+ {
+ byte[] pcmData;
- using (MemoryStream ms = new MemoryStream())
- {
- waveReader.CopyTo(ms);
- pcmData = ms.ToArray();
- }
+ 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;
}
- int chunkSize = 1600;
- int totalChunks = (pcmData.Length + chunkSize - 1) / chunkSize;
-
- if (pcmData.Length % chunkSize != 0)
+ using (MemoryStream ms = new MemoryStream())
{
- byte[] paddedData = new byte[totalChunks * chunkSize];
- Buffer.BlockCopy(pcmData, 0, paddedData, 0, pcmData.Length);
- pcmData = paddedData;
+ waveReader.CopyTo(ms);
+ pcmData = ms.ToArray();
}
+ }
- Task.Run(() =>
- {
- audioManager.AddTalkgroupStream(cpgChannel.Tgid, pcmData);
- });
+ 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;
+ }
- DateTime startTime = DateTime.UtcNow;
+ Task.Run(() =>
+ {
+ audioManager.AddTalkgroupStream(cpgChannel.Tgid, pcmData);
+ });
- for (int i = 0; i < totalChunks; i++)
- {
- int offset = i * chunkSize;
- byte[] chunk = new byte[chunkSize];
- Buffer.BlockCopy(pcmData, offset, chunk, 0, chunkSize);
+ DateTime startTime = DateTime.UtcNow;
- channel.chunkedPCM = AudioConverter.SplitToChunks(chunk);
+ for (int i = 0; i < totalChunks; i++)
+ {
+ int offset = i * chunkSize;
+ byte[] chunk = new byte[chunkSize];
+ Buffer.BlockCopy(pcmData, offset, chunk, 0, chunkSize);
- foreach (byte[] audioChunk in channel.chunkedPCM)
+ channel.chunkedPCM = AudioConverter.SplitToChunks(chunk);
+
+ foreach (byte[] audioChunk in channel.chunkedPCM)
+ {
+ if (audioChunk.Length == PCM_SAMPLES_LENGTH)
{
- if (audioChunk.Length == PCM_SAMPLES_LENGTH)
- {
- if (cpgChannel.GetChannelMode() == Codeplug.ChannelMode.P25)
- P25EncodeAudioFrame(audioChunk, fne, channel, cpgChannel, system);
- else if (cpgChannel.GetChannelMode() == Codeplug.ChannelMode.DMR)
- DMREncodeAudioFrame(audioChunk, fne, channel, cpgChannel, system);
- }
+ if (cpgChannel.GetChannelMode() == Codeplug.ChannelMode.P25)
+ P25EncodeAudioFrame(audioChunk, fne, channel, cpgChannel, system);
+ else if (cpgChannel.GetChannelMode() == Codeplug.ChannelMode.DMR)
+ DMREncodeAudioFrame(audioChunk, fne, channel, cpgChannel, system);
}
+ }
- DateTime nextPacketTime = startTime.AddMilliseconds((i + 1) * 100);
- TimeSpan waitTime = nextPacketTime - DateTime.UtcNow;
+ 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);
+ double totalDurationMs = ((double)pcmData.Length / 16000) + 250;
+ await Task.Delay((int)totalDurationMs + 3000);
- fne.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), false);
+ fne.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), false);
- Dispatcher.Invoke(() =>
- {
- if (forHold)
- channel.PttButton.Background = ChannelBox.GRAY_GRADIENT;
- else
- channel.PageState = false;
- });
+ Dispatcher.Invoke(() =>
+ {
+ if (forHold)
+ channel.PttButton.Background = ChannelBox.GRAY_GRADIENT;
+ else
+ channel.PageState = false;
});
- }
+ });
}
}
catch (Exception ex)
@@ -1917,61 +1933,12 @@ namespace dvmconsole
{
if (globalPttState)
await Task.Delay(500);
-
- foreach (ChannelBox channel in selectedChannelsManager.GetSelectedChannels())
+ ChannelBox channel = selectedChannelsManager.PrimaryChannel;
+ if (channel == null)
{
- if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
- continue;
-
- Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
- if (system == null)
- {
- Log.WriteLine($"{channel.ChannelName} refers to an {INVALID_SYSTEM} {channel.SystemName}. {ERR_INVALID_CODEPLUG}.");
- channel.IsSelected = false;
- selectedChannelsManager.RemoveSelectedChannel(channel);
- continue;
- }
-
- Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
- if (cpgChannel == null)
- {
- Log.WriteLine($"{channel.ChannelName} refers to an {INVALID_CODEPLUG_CHANNEL}. {ERR_INVALID_CODEPLUG}.");
- channel.IsSelected = false;
- selectedChannelsManager.RemoveSelectedChannel(channel);
- continue;
- }
-
- PeerSystem fne = fneSystemManager.GetFneSystem(system.Name);
- if (fne == null)
- {
- Log.WriteLine($"{channel.ChannelName} has a {ERR_INVALID_FNE_REF}. {ERR_INVALID_CODEPLUG}.");
- channel.IsSelected = false;
- selectedChannelsManager.RemoveSelectedChannel(channel);
- continue;
- }
-
- channel.TxStreamId = fne.NewStreamId();
- if (globalPttState)
- {
- Dispatcher.Invoke(() =>
- {
- btnGlobalPtt.Background = ChannelBox.RED_GRADIENT;
- channel.PttState = true;
- });
-
- fne.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), true);
- }
- else
- {
- Dispatcher.Invoke(() =>
- {
- btnGlobalPtt.Background = ChannelBox.GRAY_GRADIENT;
- channel.PttState = false;
- });
-
- fne.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), false);
- }
+ return;
}
+ channel.PttButton_Click(sender,e);
}
///
@@ -2982,122 +2949,122 @@ namespace dvmconsole
switch (e.DUID)
{
case P25DUID.LDU1:
+ {
+ // The '62', '63', '64', '65', '66', '67', '68', '69', '6A' records are LDU1
+ if ((data[0U] == 0x62U) && (data[22U] == 0x63U) &&
+ (data[36U] == 0x64U) && (data[53U] == 0x65U) &&
+ (data[70U] == 0x66U) && (data[87U] == 0x67U) &&
+ (data[104U] == 0x68U) && (data[121U] == 0x69U) &&
+ (data[138U] == 0x6AU))
{
- // The '62', '63', '64', '65', '66', '67', '68', '69', '6A' records are LDU1
- if ((data[0U] == 0x62U) && (data[22U] == 0x63U) &&
- (data[36U] == 0x64U) && (data[53U] == 0x65U) &&
- (data[70U] == 0x66U) && (data[87U] == 0x67U) &&
- (data[104U] == 0x68U) && (data[121U] == 0x69U) &&
- (data[138U] == 0x6AU))
- {
- // The '62' record - IMBE Voice 1
- Buffer.BlockCopy(data, count, channel.netLDU1, 0, 22);
- count += 22;
-
- // The '63' record - IMBE Voice 2
- Buffer.BlockCopy(data, count, channel.netLDU1, 25, 14);
- count += 14;
-
- // The '64' record - IMBE Voice 3 + Link Control
- Buffer.BlockCopy(data, count, channel.netLDU1, 50, 17);
- byte serviceOptions = data[count + 3];
- isEmergency = (serviceOptions & 0x80) == 0x80;
- count += 17;
-
- // The '65' record - IMBE Voice 4 + Link Control
- Buffer.BlockCopy(data, count, channel.netLDU1, 75, 17);
- count += 17;
-
- // The '66' record - IMBE Voice 5 + Link Control
- Buffer.BlockCopy(data, count, channel.netLDU1, 100, 17);
- count += 17;
-
- // The '67' record - IMBE Voice 6 + Link Control
- Buffer.BlockCopy(data, count, channel.netLDU1, 125, 17);
- count += 17;
-
- // The '68' record - IMBE Voice 7 + Link Control
- Buffer.BlockCopy(data, count, channel.netLDU1, 150, 17);
- count += 17;
-
- // The '69' record - IMBE Voice 8 + Link Control
- Buffer.BlockCopy(data, count, channel.netLDU1, 175, 17);
- count += 17;
-
- // The '6A' record - IMBE Voice 9 + Low Speed Data
- Buffer.BlockCopy(data, count, channel.netLDU1, 200, 16);
- count += 16;
-
- // decode 9 IMBE codewords into PCM samples
- P25DecodeAudioFrame(channel.netLDU1, e, handler, channel, isEmergency);
- }
+ // The '62' record - IMBE Voice 1
+ Buffer.BlockCopy(data, count, channel.netLDU1, 0, 22);
+ count += 22;
+
+ // The '63' record - IMBE Voice 2
+ Buffer.BlockCopy(data, count, channel.netLDU1, 25, 14);
+ count += 14;
+
+ // The '64' record - IMBE Voice 3 + Link Control
+ Buffer.BlockCopy(data, count, channel.netLDU1, 50, 17);
+ byte serviceOptions = data[count + 3];
+ isEmergency = (serviceOptions & 0x80) == 0x80;
+ count += 17;
+
+ // The '65' record - IMBE Voice 4 + Link Control
+ Buffer.BlockCopy(data, count, channel.netLDU1, 75, 17);
+ count += 17;
+
+ // The '66' record - IMBE Voice 5 + Link Control
+ Buffer.BlockCopy(data, count, channel.netLDU1, 100, 17);
+ count += 17;
+
+ // The '67' record - IMBE Voice 6 + Link Control
+ Buffer.BlockCopy(data, count, channel.netLDU1, 125, 17);
+ count += 17;
+
+ // The '68' record - IMBE Voice 7 + Link Control
+ Buffer.BlockCopy(data, count, channel.netLDU1, 150, 17);
+ count += 17;
+
+ // The '69' record - IMBE Voice 8 + Link Control
+ Buffer.BlockCopy(data, count, channel.netLDU1, 175, 17);
+ count += 17;
+
+ // The '6A' record - IMBE Voice 9 + Low Speed Data
+ Buffer.BlockCopy(data, count, channel.netLDU1, 200, 16);
+ count += 16;
+
+ // decode 9 IMBE codewords into PCM samples
+ P25DecodeAudioFrame(channel.netLDU1, e, handler, channel, isEmergency);
}
+ }
break;
case P25DUID.LDU2:
+ {
+ // The '6B', '6C', '6D', '6E', '6F', '70', '71', '72', '73' records are LDU2
+ if ((data[0U] == 0x6BU) && (data[22U] == 0x6CU) &&
+ (data[36U] == 0x6DU) && (data[53U] == 0x6EU) &&
+ (data[70U] == 0x6FU) && (data[87U] == 0x70U) &&
+ (data[104U] == 0x71U) && (data[121U] == 0x72U) &&
+ (data[138U] == 0x73U))
{
- // The '6B', '6C', '6D', '6E', '6F', '70', '71', '72', '73' records are LDU2
- if ((data[0U] == 0x6BU) && (data[22U] == 0x6CU) &&
- (data[36U] == 0x6DU) && (data[53U] == 0x6EU) &&
- (data[70U] == 0x6FU) && (data[87U] == 0x70U) &&
- (data[104U] == 0x71U) && (data[121U] == 0x72U) &&
- (data[138U] == 0x73U))
- {
- // The '6B' record - IMBE Voice 10
- Buffer.BlockCopy(data, count, channel.netLDU2, 0, 22);
- count += 22;
-
- // The '6C' record - IMBE Voice 11
- Buffer.BlockCopy(data, count, channel.netLDU2, 25, 14);
- count += 14;
-
- // The '6D' record - IMBE Voice 12 + Encryption Sync
- Buffer.BlockCopy(data, count, channel.netLDU2, 50, 17);
- newMI[0] = data[count + 1];
- newMI[1] = data[count + 2];
- newMI[2] = data[count + 3];
- count += 17;
-
- // The '6E' record - IMBE Voice 13 + Encryption Sync
- Buffer.BlockCopy(data, count, channel.netLDU2, 75, 17);
- newMI[3] = data[count + 1];
- newMI[4] = data[count + 2];
- newMI[5] = data[count + 3];
- count += 17;
-
- // The '6F' record - IMBE Voice 14 + Encryption Sync
- Buffer.BlockCopy(data, count, channel.netLDU2, 100, 17);
- newMI[6] = data[count + 1];
- newMI[7] = data[count + 2];
- newMI[8] = data[count + 3];
- count += 17;
-
- // The '70' record - IMBE Voice 15 + Encryption Sync
- Buffer.BlockCopy(data, count, channel.netLDU2, 125, 17);
- channel.algId = data[count + 1]; // Algorithm ID
- channel.kId = (ushort)((data[count + 2] << 8) | data[count + 3]); // Key ID
- count += 17;
-
- // The '71' record - IMBE Voice 16 + Encryption Sync
- Buffer.BlockCopy(data, count, channel.netLDU2, 150, 17);
- count += 17;
-
- // The '72' record - IMBE Voice 17 + Encryption Sync
- Buffer.BlockCopy(data, count, channel.netLDU2, 175, 17);
- count += 17;
-
- // The '73' record - IMBE Voice 18 + Low Speed Data
- Buffer.BlockCopy(data, count, channel.netLDU2, 200, 16);
- count += 16;
-
- if (channel.p25Errs > 0) // temp, need to actually get errors I guess
- P25Crypto.CycleP25Lfsr(channel.mi);
- else
- Array.Copy(newMI, channel.mi, P25Defines.P25_MI_LENGTH);
-
- // decode 9 IMBE codewords into PCM samples
- P25DecodeAudioFrame(channel.netLDU2, e, handler, channel, isEmergency, P25DUID.LDU2);
- }
+ // The '6B' record - IMBE Voice 10
+ Buffer.BlockCopy(data, count, channel.netLDU2, 0, 22);
+ count += 22;
+
+ // The '6C' record - IMBE Voice 11
+ Buffer.BlockCopy(data, count, channel.netLDU2, 25, 14);
+ count += 14;
+
+ // The '6D' record - IMBE Voice 12 + Encryption Sync
+ Buffer.BlockCopy(data, count, channel.netLDU2, 50, 17);
+ newMI[0] = data[count + 1];
+ newMI[1] = data[count + 2];
+ newMI[2] = data[count + 3];
+ count += 17;
+
+ // The '6E' record - IMBE Voice 13 + Encryption Sync
+ Buffer.BlockCopy(data, count, channel.netLDU2, 75, 17);
+ newMI[3] = data[count + 1];
+ newMI[4] = data[count + 2];
+ newMI[5] = data[count + 3];
+ count += 17;
+
+ // The '6F' record - IMBE Voice 14 + Encryption Sync
+ Buffer.BlockCopy(data, count, channel.netLDU2, 100, 17);
+ newMI[6] = data[count + 1];
+ newMI[7] = data[count + 2];
+ newMI[8] = data[count + 3];
+ count += 17;
+
+ // The '70' record - IMBE Voice 15 + Encryption Sync
+ Buffer.BlockCopy(data, count, channel.netLDU2, 125, 17);
+ channel.algId = data[count + 1]; // Algorithm ID
+ channel.kId = (ushort)((data[count + 2] << 8) | data[count + 3]); // Key ID
+ count += 17;
+
+ // The '71' record - IMBE Voice 16 + Encryption Sync
+ Buffer.BlockCopy(data, count, channel.netLDU2, 150, 17);
+ count += 17;
+
+ // The '72' record - IMBE Voice 17 + Encryption Sync
+ Buffer.BlockCopy(data, count, channel.netLDU2, 175, 17);
+ count += 17;
+
+ // The '73' record - IMBE Voice 18 + Low Speed Data
+ Buffer.BlockCopy(data, count, channel.netLDU2, 200, 16);
+ count += 16;
+
+ if (channel.p25Errs > 0) // temp, need to actually get errors I guess
+ P25Crypto.CycleP25Lfsr(channel.mi);
+ else
+ Array.Copy(newMI, channel.mi, P25Defines.P25_MI_LENGTH);
+
+ // decode 9 IMBE codewords into PCM samples
+ P25DecodeAudioFrame(channel.netLDU2, e, handler, channel, isEmergency, P25DUID.LDU2);
}
+ }
break;
}
@@ -3114,4 +3081,4 @@ namespace dvmconsole
});
}
} // public partial class MainWindow : Window
-} // namespace dvmconsole
+} // namespace dvmconsole
\ No newline at end of file
diff --git a/dvmconsole/SelectedChannelsManager.cs b/dvmconsole/SelectedChannelsManager.cs
index 4ffb0c2..20d1272 100644
--- a/dvmconsole/SelectedChannelsManager.cs
+++ b/dvmconsole/SelectedChannelsManager.cs
@@ -8,6 +8,7 @@
* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0)
*
* Copyright (C) 2024 Caleb, K4PHP
+* Copyright (C) 2025 Steven Jennison, KD8RHO
*
*/
@@ -20,8 +21,11 @@ namespace dvmconsole
///
public class SelectedChannelsManager
{
+ private ChannelBox primaryChannel;
+
private readonly HashSet selectedChannels;
+ public ChannelBox PrimaryChannel => primaryChannel;
public IReadOnlyCollection GetSelectedChannels() => selectedChannels;
/*
@@ -33,6 +37,10 @@ namespace dvmconsole
///
public event Action SelectedChannelsChanged;
+ ///
+ /// Triggered when primary channel is changed
+ ///
+ public event Action PrimaryChannelChanged;
/*
** Methods
*/
@@ -54,7 +62,7 @@ namespace dvmconsole
if (selectedChannels.Add(channel))
{
channel.IsSelected = true;
- SelectedChannelsChanged.Invoke();
+ SelectedChannelsChanged?.Invoke();
}
}
@@ -66,8 +74,13 @@ namespace dvmconsole
{
if (selectedChannels.Remove(channel))
{
+ if (primaryChannel == channel)
+ {
+ ClearPrimaryChannel();
+ }
+ channel.IsPrimary = false;
channel.IsSelected = false;
- SelectedChannelsChanged.Invoke();
+ SelectedChannelsChanged?.Invoke();
}
}
@@ -80,7 +93,27 @@ namespace dvmconsole
channel.IsSelected = false;
selectedChannels.Clear();
- SelectedChannelsChanged.Invoke();
+ SelectedChannelsChanged?.Invoke();
+ }
+
+ ///
+ /// Sets primary channel to the passed ChannelBox
+ ///
+ ///
+ public void SetPrimaryChannel(ChannelBox channel)
+ {
+ Log.WriteLine($"Setting primary channel to {channel.ChannelName}");
+ primaryChannel = channel;
+ PrimaryChannelChanged?.Invoke();
+ }
+
+ ///
+ /// Clears the primary channel selection, setting it to null
+ ///
+ public void ClearPrimaryChannel()
+ {
+ primaryChannel = null;
+ PrimaryChannelChanged?.Invoke();
}
} // public class SelectedChannelsManager
} // namespace dvmconsole