Give the network status window N1MM's tabs and columns

The window was one flat table of columns this program made up. N1MM's has
tabs and a fixed set of columns, and an operator who knows one has to be
able to read the other.

**The window** now has Stations, Actions and Messages. Stations is a grid
with N1MM's headers in N1MM's order — Computer, IP Address, Pass, Run,
10, 100, Freq, Op, Msg, Send, Receive, Band, R/M, Mode — and a row per
station, this computer included. Right-click a row to pass the contact
being worked to that station, to talk, to show the connection log, or to
close and open its connection. Actions holds the talk line, the pass
frequency, the echo and the networking switch; Messages holds the chat,
the connection failures, the echo round trips and a trace of every
message that arrives.

**`STATUS`** is what fills those columns. N1MM sends it every few seconds
whether anything has changed or not — the pass frequency, the frequency,
run or search and pounce, the operator, the rates over the last ten and
the last hundred contacts, the mode, the categories and the version — so
a station sitting on one frequency for an hour still has a current row.
`BANDMAP` only says a station moved. The five transmit-interlock flags
are sent as false: this program does not block a transmitter.

**`Rates.PerHour`** reads the two rate columns out of the log. Count back
over the last N contacts, stop at a gap longer than the contest's minimum
off time, and treat a span under a minute as a minute, so three contacts
in five seconds read as 180 an hour rather than 2160.

A connection closed by hand stays closed. Without that flag the next
beacon opens it again within ten seconds, which makes the menu item look
broken.

Left out, because there is nothing behind them: N1MM's Resynchronize and
Rescore tabs, the transmit interlocks of its Options tab, the Master
column and the call-stacking target tab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
This commit is contained in:
2026-09-03 21:39:42 +00:00
co-authored by Claude Opus 5
parent 6f2f39768c
commit 68b29d722a
12 changed files with 756 additions and 105 deletions
+82 -1
View File
@@ -88,6 +88,10 @@ public sealed class StationLink : IDisposable
set => mine.Operator = value;
}
/// This computer's own row, which is in the station list as well. The
/// window reads its pass frequency and its counts off it.
public NetworkedStation Me => mine;
/// Every station, this computer included, in the order they were first
/// heard.
public IReadOnlyList<NetworkedStation> Stations => stations.Values.ToList();
@@ -104,6 +108,11 @@ public sealed class StationLink : IDisposable
public event EventHandler<string>? Failed;
/// Every message that arrives, whatever it says. Nothing needs this to run:
/// it is what the messages pane of the network status window prints when
/// the operator turns tracing on.
public event EventHandler<StationRecord>? MessageArrived;
public void Start()
{
if (loops.Count > 0)
@@ -257,6 +266,62 @@ public sealed class StationLink : IDisposable
cancellation);
}
/// Everything this station has to say about itself, which is what fills
/// the other operators' network status windows. It goes out every few
/// seconds whether anything has changed or not.
public Task<int> SendStatusAsync(StationStatus status, CancellationToken cancellation = default)
{
mine.PassFrequency = status.PassFrequency;
mine.Frequency = status.Frequency;
mine.IsRunning = status.IsRunning;
mine.Operator = status.Operator;
mine.Mode = status.Mode;
mine.Last10 = status.Last10;
mine.Last100 = status.Last100;
mine.IsRun2 = status.IsRun2;
StationsChanged?.Invoke(this, EventArgs.Empty);
return SendAsync(StationMessages.Status(status, version, StationNumber, computerName), cancellation);
}
/// A frequency this station is handing to the others, and who is on it.
/// With `to` set it goes to that station alone, which is how an operator
/// passes one contact to one position; without it every station is told,
/// which is how the pass frequency is announced.
public Task<int> SendPassAsync(
Frequency frequency,
string call,
NetworkedStation? to = null,
CancellationToken cancellation = default)
{
mine.PassFrequency = frequency;
mine.PassCall = call;
StationsChanged?.Invoke(this, EventArgs.Empty);
StationRecord record = StationMessages.PassFrequency(frequency, call, StationNumber, computerName);
return to is null
? SendAsync(record, cancellation)
: Sent(SendToAsync(to, record, cancellation));
}
/// Closes the connection to one station because the operator asked. It
/// stays closed: a beacon from that station opens a new one, so without
/// this flag the connection would be back within ten seconds.
public void Close(NetworkedStation station)
{
station.IsReconnectPrevented = true;
Drop(station.ComputerName, "closed by the operator");
}
/// Opens a connection to a station the operator closed, or to one whose
/// connection has not come back on its own.
public void Open(NetworkedStation station)
{
station.IsReconnectPrevented = false;
if (!station.IsMine && !writers.ContainsKey(station.ComputerName))
{
_ = ConnectAsync(station, stopping.Token);
}
}
/// Asks every station whether it is there. The answer sets `EchoTime`.
public Task<int> SendEchoRequestAsync(CancellationToken cancellation = default)
{
@@ -264,6 +329,10 @@ public sealed class StationLink : IDisposable
return SendAsync(StationMessages.EchoRequest(StationNumber, computerName, DateTime.UtcNow), cancellation);
}
/// One send read as the count every other send returns.
private static async Task<int> Sent(Task<bool> one) =>
await one.ConfigureAwait(false) ? 1 : 0;
public void Dispose()
{
stopping.Cancel();
@@ -365,7 +434,7 @@ public sealed class StationLink : IDisposable
}
station.Refused = "";
StationsChanged?.Invoke(this, EventArgs.Empty);
if (!writers.ContainsKey(station.ComputerName))
if (!station.IsReconnectPrevented && !writers.ContainsKey(station.ComputerName))
{
_ = ConnectAsync(station, stopping.Token);
}
@@ -465,6 +534,7 @@ public sealed class StationLink : IDisposable
station.LastMessage = record.Type;
station.Read++;
mine.Read++;
MessageArrived?.Invoke(this, record);
switch (record.Type)
{
case "IAM":
@@ -476,6 +546,17 @@ public sealed class StationLink : IDisposable
station.IsRunning = record.Flag(2);
station.RadioNumber = Math.Max(1, record.Number(3));
break;
case "STATUS":
StationStatus status = StationMessages.ReadStatus(record);
station.PassFrequency = status.PassFrequency;
station.Frequency = status.Frequency;
station.IsRunning = status.IsRunning;
station.Operator = status.Operator;
station.Mode = status.Mode;
station.Last10 = status.Last10;
station.Last100 = status.Last100;
station.IsRun2 = status.IsRun2;
break;
case "XMIT":
station.IsTransmitting = record.Flag(0);
station.RadioNumber = Math.Max(1, record.Number(1));