diff --git a/WhackerLinkConsoleV2/Assets/dualpage.png b/WhackerLinkConsoleV2/Assets/dualpage.png
new file mode 100644
index 0000000..7ab2bf5
Binary files /dev/null and b/WhackerLinkConsoleV2/Assets/dualpage.png differ
diff --git a/WhackerLinkConsoleV2/Assets/pager.png b/WhackerLinkConsoleV2/Assets/pager.png
new file mode 100644
index 0000000..b5f4353
Binary files /dev/null and b/WhackerLinkConsoleV2/Assets/pager.png differ
diff --git a/WhackerLinkConsoleV2/ChannelBox.xaml.cs b/WhackerLinkConsoleV2/ChannelBox.xaml.cs
index 18c9d2d..f6750c9 100644
--- a/WhackerLinkConsoleV2/ChannelBox.xaml.cs
+++ b/WhackerLinkConsoleV2/ChannelBox.xaml.cs
@@ -336,5 +336,10 @@ private void PttButton_MouseLeave(object sender, System.Windows.Input.MouseEvent
((Button)sender).Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FFDDDDDD"));
}
+
+ private void SetPageState(bool state)
+ {
+ PageState = state;
+ }
}
}
diff --git a/WhackerLinkConsoleV2/MainWindow.xaml b/WhackerLinkConsoleV2/MainWindow.xaml
index 3cc66f8..58b36fb 100644
--- a/WhackerLinkConsoleV2/MainWindow.xaml
+++ b/WhackerLinkConsoleV2/MainWindow.xaml
@@ -41,8 +41,11 @@
-
-
+
+
+
+
+
diff --git a/WhackerLinkConsoleV2/MainWindow.xaml.cs b/WhackerLinkConsoleV2/MainWindow.xaml.cs
index d9098ad..c8c75c0 100644
--- a/WhackerLinkConsoleV2/MainWindow.xaml.cs
+++ b/WhackerLinkConsoleV2/MainWindow.xaml.cs
@@ -49,6 +49,7 @@
using NWaves.Signals;
using static WhackerLinkConsoleV2.P25Crypto;
using static WhackerLinkLib.Models.Radio.Codeplug;
+using System.Threading.Channels;
namespace WhackerLinkConsoleV2
{
@@ -67,6 +68,9 @@ public partial class MainWindow : Window
private SettingsManager _settingsManager = new SettingsManager();
private SelectedChannelsManager _selectedChannelsManager;
+
+ private List<(double ToneA, double ToneB)> _selectedToneSets = new List<(double, double)>();
+
private FlashingBackgroundManager _flashingManager;
private WaveFilePlaybackManager _emergencyAlertPlayback;
private WebSocketManager _webSocketManager = new WebSocketManager();
@@ -124,6 +128,11 @@ public MainWindow()
Loaded += MainWindow_Loaded;
}
+ private class YamlConfig
+ {
+ public List Tones { get; set; }
+ }
+
private void OpenCodeplug_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog openFileDialog = new OpenFileDialog
@@ -303,8 +312,7 @@ private void GenerateChannelWidgets()
systemStatusBox.ConnectionState = "Disconnected";
}
});
- } else
- {
+ } else {
_fneSystemManager.AddFneSystem(system.Name, system, this);
PeerSystem peer = _fneSystemManager.GetFneSystem(system.Name);
@@ -344,6 +352,7 @@ private void GenerateChannelWidgets()
}
}
+
if (_settingsManager.ShowChannels && Codeplug != null)
{
foreach (var zone in Codeplug.Zones)
@@ -415,6 +424,137 @@ private void GenerateChannelWidgets()
}
}
+ // Add ToneSet boxes if enabled
+ if (_settingsManager.ShowQCTones && Codeplug != null && Codeplug?.Tones != null)
+ {
+ foreach (var tone in Codeplug.Tones)
+ {
+ var toneSetControl = new ToneSet(tone.Name, tone.ToneA, tone.ToneB);
+
+ // Hook up events
+ toneSetControl.PlayClicked += async (s, e) =>
+ {
+ if (_selectedToneSets.Count > 0)
+ {
+ var selectedTones = _selectedToneSets.ToList();
+ var initiallyActiveChannels = _selectedChannelsManager
+ .GetSelectedChannels()
+ .Where(c => c.PageState)
+ .ToList();
+
+ for (int i = 0; i < selectedTones.Count; i++)
+ {
+ var selected = selectedTones[i];
+ var key = (selected.ToneA, selected.ToneB);
+
+ // Check if any of the original channels have lost their PageState
+ var prematurelyCleared = initiallyActiveChannels
+ .Where(c => !c.PageState)
+ .ToList();
+
+ if (prematurelyCleared.Any())
+ {
+ // Pause logic: restore PageState for affected channels
+ foreach (var ch in prematurelyCleared)
+ {
+ ch.PageState = true;
+ Dispatcher.Invoke(() =>
+ {
+ ch.PageSelectButton.Background = ch.orangeGradient;
+ });
+ }
+
+ // Wait a moment to visually reflect the reset before continuing
+ await Task.Delay(300);
+ }
+
+ // Play the tone
+ await PlayTone(selected.ToneA.ToString(), selected.ToneB.ToString());
+
+ // Remove from selected set
+ _selectedToneSets.Remove(key);
+
+ // Deselect the tone visually
+ foreach (var child in ChannelsCanvas.Children)
+ {
+ if (child is ToneSet ts && ts.ToneA == key.ToneA && ts.ToneB == key.ToneB)
+ {
+ ts.SetSelected(false);
+ break;
+ }
+ }
+ }
+
+ // Now that all tones are done, clean up page state
+ foreach (var ch in initiallyActiveChannels)
+ {
+ ch.PageState = false;
+ ch.PageSelectButton.Background = ch.grayGradient;
+ }
+ }
+ else
+ {
+ var hasActivePage = _selectedChannelsManager.GetSelectedChannels().Any(c => c.PageState);
+ if (hasActivePage)
+ {
+ await PlayTone(tone.ToneA.ToString(), tone.ToneB.ToString());
+
+ // Clear PageState after this tone
+ foreach (var channel in _selectedChannelsManager.GetSelectedChannels())
+ {
+ channel.PageState = false;
+ channel.PageSelectButton.Background = channel.grayGradient;
+ }
+ }
+ }
+ };
+
+
+ toneSetControl.SelectToggled += (s, e) =>
+ {
+ var key = (tone.ToneA, tone.ToneB);
+ if (_selectedToneSets.Contains(key))
+ {
+ _selectedToneSets.Remove(key);
+ toneSetControl.SetSelected(false);
+ }
+ else
+ {
+ _selectedToneSets.Add(key);
+ toneSetControl.SetSelected(true);
+ }
+ };
+
+ // Position like ToneSet Boxes using same layout logic
+ if (_settingsManager.QCToneSetPositions.TryGetValue(tone.Name, out var position))
+ {
+ Canvas.SetLeft(toneSetControl, position.X);
+ Canvas.SetTop(toneSetControl, position.Y);
+ }
+ else
+ {
+ Canvas.SetLeft(toneSetControl, offsetX);
+ Canvas.SetTop(toneSetControl, offsetY);
+ }
+
+ toneSetControl.MouseLeftButtonDown += ToneSet_MouseLeftButtonDown;
+ toneSetControl.MouseMove += ToneSet_MouseMove;
+ toneSetControl.MouseRightButtonDown += ToneSet_MouseRightButtonDown;
+
+ ChannelsCanvas.Children.Add(toneSetControl);
+
+ offsetX += 225;
+
+ if (offsetX + 220 > ChannelsCanvas.ActualWidth)
+ {
+ offsetX = 20;
+ offsetY += 106;
+ }
+ }
+ }
+
+
+
playbackChannelBox = new ChannelBox(_selectedChannelsManager, _audioManager, PLAYBACKCHNAME, PLAYBACKSYS, PLAYBACKTG);
if (_settingsManager.ChannelPositions.TryGetValue(PLAYBACKCHNAME, out var pos))
@@ -448,6 +588,95 @@ private void GenerateChannelWidgets()
AdjustCanvasHeight();
}
+ private const int GridSize = 5;
+
+ private void ToneSet_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ if (!isEditMode || !(sender is UIElement element)) return;
+
+ _draggedElement = element;
+ _startPoint = e.GetPosition(ChannelsCanvas);
+ _offsetX = _startPoint.X - Canvas.GetLeft(_draggedElement);
+ _offsetY = _startPoint.Y - Canvas.GetTop(_draggedElement);
+ _isDragging = true;
+
+ element.CaptureMouse();
+ }
+
+ private void ToneSet_MouseMove(object sender, MouseEventArgs e)
+ {
+ if (!isEditMode || !_isDragging || _draggedElement == null) return;
+
+ Point currentPosition = e.GetPosition(ChannelsCanvas);
+
+ // Calculate the new position with snapping to the grid
+ double newLeft = Math.Round((currentPosition.X - _offsetX) / GridSize) * GridSize;
+ double newTop = Math.Round((currentPosition.Y - _offsetY) / GridSize) * GridSize;
+
+ // 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 ToneSet
+ if (_draggedElement is ToneSet toneSet)
+ {
+ _settingsManager.UpdateQCToneSetPosition(toneSet.ToneName, newLeft, newTop);
+ }
+
+ AdjustCanvasHeight();
+ }
+
+ private void ToneSet_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ if (!isEditMode || !_isDragging || _draggedElement == null) return;
+
+ _isDragging = false;
+ _draggedElement.ReleaseMouseCapture();
+ _draggedElement = null;
+ }
+
+
+ private async void ToneSet_PlayClicked(object sender, EventArgs e)
+ {
+ if (sender is ToneSet toneSet)
+ {
+ if (_selectedToneSets.Count > 0)
+ {
+ foreach (var selected in _selectedToneSets)
+ {
+ await PlayTone(selected.ToneA.ToString(), selected.ToneB.ToString());
+ }
+ }
+ else
+ {
+ await PlayTone(toneSet.ToneA.ToString(), toneSet.ToneB.ToString());
+ }
+ }
+ }
+
+ private void ToneSet_SelectClicked(object sender, EventArgs e)
+ {
+ if (sender is ToneSet toneSet)
+ {
+ var key = (toneSet.ToneA, toneSet.ToneB);
+
+ if (_selectedToneSets.Contains(key))
+ {
+ _selectedToneSets.Remove(key);
+ toneSet.SetSelected(false);
+ }
+ else
+ {
+ _selectedToneSets.Add(key);
+ toneSet.SetSelected(true);
+ }
+ }
+ }
+
private void WaveIn_RecordingStopped(object sender, EventArgs e)
{
/* stub */
@@ -934,6 +1163,7 @@ private void SelectWidgets_Click(object sender, RoutedEventArgs e)
_settingsManager.ShowSystemStatus = widgetSelectionWindow.ShowSystemStatus;
_settingsManager.ShowChannels = widgetSelectionWindow.ShowChannels;
_settingsManager.ShowAlertTones = widgetSelectionWindow.ShowAlertTones;
+ _settingsManager.ShowQCTones = widgetSelectionWindow.ShowQCTones;
GenerateChannelWidgets();
_settingsManager.SaveSettings();
@@ -1301,8 +1531,6 @@ private void ChannelBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs
element.CaptureMouse();
}
- private const int GridSize = 5;
-
private void ChannelBox_MouseMove(object sender, MouseEventArgs e)
{
if (!isEditMode || !_isDragging || _draggedElement == null) return;
@@ -1455,6 +1683,7 @@ private void MainWindow_Loaded(object sender, RoutedEventArgs e)
if (!string.IsNullOrEmpty(_settingsManager.LastCodeplugPath) && File.Exists(_settingsManager.LastCodeplugPath))
{
LoadCodeplug(_settingsManager.LastCodeplugPath);
+ //LoadToneSets();
}
else
{
@@ -1462,6 +1691,116 @@ private void MainWindow_Loaded(object sender, RoutedEventArgs e)
}
}
+ private async Task PlayTone(string ToneA, string ToneB)
+ {
+ var selectedChannels = _selectedChannelsManager.GetSelectedChannels();
+
+ // Check if any selected channel has PageState = true
+ if (!selectedChannels.Any(ch => ch.PageState))
+ {
+ // No channels with active page state - do nothing
+ return;
+ }
+
+ foreach (ChannelBox channel in selectedChannels)
+ {
+ if (!channel.PageState)
+ continue; // skip channels without page state
+
+ Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
+ Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
+
+ ToneGenerator generator = new ToneGenerator();
+
+ double toneADuration = 1.0;
+ double toneBDuration = 3.0;
+
+ byte[] toneA = generator.GenerateTone(double.Parse(ToneA), toneADuration);
+ byte[] toneB = generator.GenerateTone(double.Parse(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 = system.IsDvm ? 320 : 1600;
+ int totalChunks = (combinedAudio.Length + chunkSize - 1) / chunkSize;
+
+ _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 (!system.IsDvm)
+ {
+ IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name);
+
+ AudioPacket voicePacket = new AudioPacket
+ {
+ Data = chunk,
+ VoiceChannel = new VoiceChannel
+ {
+ Frequency = channel.VoiceChannel,
+ DstId = cpgChannel.Tgid,
+ SrcId = system.Rid,
+ Site = system.Site
+ },
+ Site = system.Site,
+ LopServerVocode = true
+ };
+
+ handler.SendMessage(voicePacket.GetData());
+ }
+ else
+ {
+ PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
+
+ if (chunk.Length == 320)
+ {
+ P25EncodeAudioFrame(chunk, handler, channel, cpgChannel, system);
+ }
+ }
+ }
+ });
+
+ double totalDurationMs = (toneADuration + toneBDuration) * 1000 + 750;
+ await Task.Delay((int)totalDurationMs);
+
+ if (!system.IsDvm)
+ {
+ IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name);
+
+ GRP_VCH_RLS release = new GRP_VCH_RLS
+ {
+ SrcId = system.Rid,
+ DstId = cpgChannel.Tgid,
+ Channel = channel.VoiceChannel,
+ Site = system.Site
+ };
+
+ handler.SendMessage(release.GetData());
+ }
+ else
+ {
+ PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
+
+ await Task.Delay(4000);
+ handler.SendP25TDU(uint.Parse(system.Rid), uint.Parse(cpgChannel.Tgid), false);
+ }
+
+ Dispatcher.Invoke(() =>
+ {
+ channel.PageSelectButton.Background = channel.grayGradient;
+ });
+ }
+ }
+
private async void OnHoldTimerElapsed(object sender, ElapsedEventArgs e)
{
foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels())
diff --git a/WhackerLinkConsoleV2/SettingsManager.cs b/WhackerLinkConsoleV2/SettingsManager.cs
index 96690b9..5e3f682 100644
--- a/WhackerLinkConsoleV2/SettingsManager.cs
+++ b/WhackerLinkConsoleV2/SettingsManager.cs
@@ -30,6 +30,7 @@ public class SettingsManager
public bool ShowSystemStatus { get; set; } = true;
public bool ShowChannels { get; set; } = true;
public bool ShowAlertTones { get; set; } = true;
+ public bool ShowQCTones { get; set; } = true;
public string LastCodeplugPath { get; set; } = null;
@@ -37,6 +38,7 @@ public class SettingsManager
public Dictionary SystemStatusPositions { get; set; } = new Dictionary();
public List AlertToneFilePaths { get; set; } = new List();
public Dictionary AlertTonePositions { get; set; } = new Dictionary();
+ public Dictionary QCToneSetPositions { get; set; } = new Dictionary();
public Dictionary ChannelOutputDevices { get; set; } = new Dictionary();
public void LoadSettings()
@@ -53,11 +55,13 @@ public void LoadSettings()
ShowSystemStatus = loadedSettings.ShowSystemStatus;
ShowChannels = loadedSettings.ShowChannels;
ShowAlertTones = loadedSettings.ShowAlertTones;
+ ShowQCTones = loadedSettings.ShowQCTones;
LastCodeplugPath = loadedSettings.LastCodeplugPath;
ChannelPositions = loadedSettings.ChannelPositions ?? new Dictionary();
SystemStatusPositions = loadedSettings.SystemStatusPositions ?? new Dictionary();
AlertToneFilePaths = loadedSettings.AlertToneFilePaths ?? new List();
AlertTonePositions = loadedSettings.AlertTonePositions ?? new Dictionary();
+ QCToneSetPositions = loadedSettings.QCToneSetPositions ?? new Dictionary();
ChannelOutputDevices = loadedSettings.ChannelOutputDevices ?? new Dictionary();
}
}
@@ -88,6 +92,12 @@ public void UpdateChannelPosition(string channelName, double x, double y)
SaveSettings();
}
+ public void UpdateQCToneSetPosition(string name, double x, double y)
+ {
+ QCToneSetPositions[name] = new ChannelPosition { X = x, Y = y };
+ SaveSettings();
+ }
+
public void UpdateSystemStatusPosition(string systemName, double x, double y)
{
SystemStatusPositions[systemName] = new ChannelPosition { X = x, Y = y };
diff --git a/WhackerLinkConsoleV2/ToneSet.xaml b/WhackerLinkConsoleV2/ToneSet.xaml
new file mode 100644
index 0000000..b3cc0bc
--- /dev/null
+++ b/WhackerLinkConsoleV2/ToneSet.xaml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/WhackerLinkConsoleV2/ToneSet.xaml.cs b/WhackerLinkConsoleV2/ToneSet.xaml.cs
new file mode 100644
index 0000000..66e2c14
--- /dev/null
+++ b/WhackerLinkConsoleV2/ToneSet.xaml.cs
@@ -0,0 +1,72 @@
+using System;
+using System.ComponentModel;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+
+namespace WhackerLinkConsoleV2.Controls
+{
+ public partial class ToneSet : UserControl, INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public string ToneName { get; set; }
+ public double ToneA { get; set; }
+ public double ToneB { get; set; }
+
+ public bool IsEditMode { get; set; }
+
+ public event EventHandler PlayClicked;
+ public event EventHandler SelectToggled;
+
+ private bool _isSelected = false;
+
+ public ToneSet(string toneName, double toneA, double toneB)
+ {
+ InitializeComponent();
+ ToneName = toneName;
+ ToneA = toneA;
+ ToneB = toneB;
+
+ MouseLeftButtonDown += ToneSet_MouseLeftButtonDown;
+
+ DataContext = this;
+ }
+
+ private void ToneSet_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ if (IsEditMode) return;
+ }
+
+
+ private void ToneSetPlayBtn_Click(object sender, RoutedEventArgs e)
+ {
+ PlayClicked?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void ToneSetSelectBtn_Click(object sender, RoutedEventArgs e)
+ {
+ SelectToggled?.Invoke(this, EventArgs.Empty);
+ }
+
+ public void SetSelected(bool selected)
+ {
+ _isSelected = selected;
+ UpdateSelectButton();
+ if (selected)
+ this.Background = System.Windows.Media.Brushes.LightBlue;
+ else
+ this.Background = System.Windows.Media.Brushes.Transparent;
+ }
+
+ private void UpdateSelectButton()
+ {
+ //if (ToneSetSelectBtn != null)
+ {
+ //ToneSetSelectBtn.Content = _isSelected ? "Deselect" : "Select";
+ //ToneSetSelectBtn.Background = _isSelected ? Brushes.LightGreen : Brushes.LightGray;
+ }
+ }
+ }
+}
diff --git a/WhackerLinkConsoleV2/WhackerLinkConsoleV2.csproj b/WhackerLinkConsoleV2/WhackerLinkConsoleV2.csproj
index 5441dd9..77f5a9e 100644
--- a/WhackerLinkConsoleV2/WhackerLinkConsoleV2.csproj
+++ b/WhackerLinkConsoleV2/WhackerLinkConsoleV2.csproj
@@ -13,6 +13,8 @@
+
+
@@ -71,6 +73,8 @@
+
+
diff --git a/WhackerLinkConsoleV2/WhackerLinkConsoleV2.csproj.user b/WhackerLinkConsoleV2/WhackerLinkConsoleV2.csproj.user
new file mode 100644
index 0000000..0f14913
--- /dev/null
+++ b/WhackerLinkConsoleV2/WhackerLinkConsoleV2.csproj.user
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/WhackerLinkConsoleV2/WidgetSelectionWindow.xaml b/WhackerLinkConsoleV2/WidgetSelectionWindow.xaml
index d2cd7e3..183fa9a 100644
--- a/WhackerLinkConsoleV2/WidgetSelectionWindow.xaml
+++ b/WhackerLinkConsoleV2/WidgetSelectionWindow.xaml
@@ -7,6 +7,7 @@
+
diff --git a/WhackerLinkConsoleV2/WidgetSelectionWindow.xaml.cs b/WhackerLinkConsoleV2/WidgetSelectionWindow.xaml.cs
index d837eb0..9826dda 100644
--- a/WhackerLinkConsoleV2/WidgetSelectionWindow.xaml.cs
+++ b/WhackerLinkConsoleV2/WidgetSelectionWindow.xaml.cs
@@ -27,6 +27,7 @@ public partial class WidgetSelectionWindow : Window
public bool ShowSystemStatus { get; private set; } = true;
public bool ShowChannels { get; private set; } = true;
public bool ShowAlertTones { get; private set; } = true;
+ public bool ShowQCTones { get; private set; } = true;
public WidgetSelectionWindow()
{
@@ -38,6 +39,8 @@ private void ApplyButton_Click(object sender, RoutedEventArgs e)
ShowSystemStatus = SystemStatusCheckBox.IsChecked ?? false;
ShowChannels = ChannelCheckBox.IsChecked ?? false;
ShowAlertTones = AlertToneCheckBox.IsChecked ?? false;
+ ShowQCTones = QCTonesCheckBox.IsChecked ?? false;
+
DialogResult = true;
Close();
}
diff --git a/WhackerLinkConsoleV2/codeplugs/codeplug.yml b/WhackerLinkConsoleV2/codeplugs/codeplug.yml
index ca0f856..820532d 100644
--- a/WhackerLinkConsoleV2/codeplugs/codeplug.yml
+++ b/WhackerLinkConsoleV2/codeplugs/codeplug.yml
@@ -44,6 +44,17 @@ systems:
systemID: 1
siteID: 1
range: 1.5
+tones:
+ - name: "Station 1"
+ toneA: 855.5
+ toneB: 349
+ zones:
+ - "Zone 1"
+ - name: "Station 2"
+ toneA: 1006.9
+ toneB: 500.9
+ zones:
+ - "Zone 1"
zones:
- name: "Zone 1"
@@ -71,4 +82,4 @@ zones:
tgid: "16002"
- name: "Channel C"
system: "System 1"
- tgid: "16002"
\ No newline at end of file
+ tgid: "16002"