Show the cluster's solar weather in a bandmap status bar

The reader follows N1MM's rules for WWV and WCY lines and the sh/wwv
table. The bar is hidden until the cluster has sent some.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 12:12:35 +00:00
co-authored by Claude Sonnet 5.5
parent 1e0fb3eb08
commit e55159ba49
6 changed files with 133 additions and 0 deletions
+15
View File
@@ -914,6 +914,7 @@ public sealed partial class AppSession : IDisposable
public void ConnectCluster()
{
cluster?.Dispose();
SetSolar("");
cluster = new ClusterClient(
Settings.ClusterHost,
Settings.ClusterPort,
@@ -923,14 +924,28 @@ public sealed partial class AppSession : IDisposable
autoLogon: Settings.ClusterAutoLogon,
keepAliveInterval: TimeSpan.FromMinutes(Math.Max(1, Settings.ClusterKeepAliveMinutes)));
cluster.SpotArrived += (_, spot) => TakeSpot(spot);
cluster.SolarArrived += (_, text) => SetSolar(text);
cluster.ConnectionChanged += (_, _) => Changed?.Invoke(this, EventArgs.Empty);
cluster.Start();
}
/// The latest solar weather the cluster sent, or empty when it has sent
/// none on this connection.
public string Solar { get; private set; } = "";
public event EventHandler? SolarChanged;
private void SetSolar(string text)
{
Solar = text;
SolarChanged?.Invoke(this, EventArgs.Empty);
}
public void DisconnectCluster()
{
cluster?.Dispose();
cluster = null;
SetSolar("");
Changed?.Invoke(this, EventArgs.Empty);
}
@@ -8,6 +8,10 @@
<ComboBox Name="SpanBox" FontSize="11" Width="96" />
<CheckBox Name="CentreBox" Content="Centre on VFO" FontSize="11" Margin="8,0,0,0" />
</DockPanel>
<Border Name="SolarBar" DockPanel.Dock="Bottom" IsVisible="False" Padding="6,2"
BorderThickness="0,1,0,0" BorderBrush="Gray">
<TextBlock Name="SolarText" FontSize="11" TextWrapping="Wrap" />
</Border>
<local:BandmapView Name="View" />
</DockPanel>
</local:RefreshableWindow>
@@ -76,9 +76,21 @@ public sealed partial class BandmapWindow : RefreshableWindow
};
View.FrequencyPicked += (_, frequency) => tune(frequency, "");
session.Bandmap.Changed += (_, _) => Dispatcher.UIThread.Post(Refresh);
EventHandler solarChanged = (_, _) => Dispatcher.UIThread.Post(ShowSolar);
session.SolarChanged += solarChanged;
Closed += (_, _) => session.SolarChanged -= solarChanged;
ShowSolar();
Refresh();
}
/// The status bar holds the solar weather the cluster sent, and is hidden
/// while there is none.
private void ShowSolar()
{
SolarText.Text = session.Solar;
SolarBar.IsVisible = session.Solar.Length > 0;
}
private void SaveView()
{
session.Save(session.Settings with
+8
View File
@@ -29,6 +29,7 @@ public sealed class ClusterClient : IDisposable
private readonly CancellationTokenSource stopping = new();
private readonly LineAssembler lines = new();
private readonly Queue<ClusterLine> recent = new();
private readonly SolarReader solar = new();
private readonly Lock guard = new();
private TcpClient? client;
@@ -80,6 +81,9 @@ public sealed class ClusterClient : IDisposable
public event EventHandler<string>? LineArrived;
/// The node sent solar weather, as the text to show.
public event EventHandler<string>? SolarArrived;
/// What went out to the node, so the telnet window can show the login and
/// the commands beside the node's answers.
public event EventHandler<string>? LineSent;
@@ -237,6 +241,10 @@ public sealed class ClusterClient : IDisposable
Kind = kind;
ConnectionChanged?.Invoke(this, IsConnected);
}
if (solar.Read(line) is { } weather)
{
SolarArrived?.Invoke(this, weather);
}
Spot? spot = SpotLine.Parse(line, DateTime.UtcNow);
if (spot is not null)
{
+60
View File
@@ -0,0 +1,60 @@
using System.Text.RegularExpressions;
namespace Nonemm.Spotting;
/// Picks the solar weather out of a node's traffic, as N1MM does for its Info
/// window. A node sends it as one line, such as `WWV de VE7CC <18Z> : SFI=140,
/// A=5, K=2`, or after `sh/wwv` as a header line (`Date Hour SFI A K ...`)
/// and a line of numbers under it. The text is kept as the node sent it,
/// without the extra blanks.
public sealed partial class SolarReader
{
private bool afterHeader;
/// The text to show when the line carries solar weather, otherwise null.
/// A line has to be passed in even when it is not solar weather, because
/// a spot between the header and the numbers cancels the header.
public string? Read(string line)
{
string text = Collapse(line);
if (text.Length == 0)
{
return null;
}
string upper = text.ToUpperInvariant();
bool isFromNode = upper.Contains(" DE ", StringComparison.Ordinal);
if (upper.StartsWith("DX", StringComparison.Ordinal))
{
afterHeader = false;
return null;
}
if (upper.StartsWith("WWV:", StringComparison.Ordinal)
|| upper.StartsWith("WCY:", StringComparison.Ordinal)
|| upper.Contains("WWV DE", StringComparison.Ordinal)
|| upper.Contains("WCY DE", StringComparison.Ordinal))
{
return text;
}
if (upper.StartsWith("DATE HOUR SFI", StringComparison.Ordinal))
{
afterHeader = true;
return null;
}
if (afterHeader && !isFromNode)
{
return "SFI/A/K: " + text;
}
if (isFromNode)
{
afterHeader = false;
}
return null;
}
/// Control characters out, runs of blanks down to one.
private static string Collapse(string line) =>
Blanks().Replace(new string([.. line.Where(c => !char.IsControl(c) || c == '\t')]), " ").Trim();
[GeneratedRegex(@"\s+")]
private static partial Regex Blanks();
}