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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user