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
+43 -1
View File
@@ -19,6 +19,11 @@ namespace Nonemm.App;
/// `Changed` event tells them all to redraw.
public sealed class AppSession : IDisposable
{
/// How often this station tells the others how it is doing. N1MM sends its
/// status on the same sort of interval; every second would read the log
/// sixty times a minute for two numbers that barely move.
private static readonly TimeSpan StatusInterval = TimeSpan.FromSeconds(5);
private LogStore? store;
private readonly List<RigctldRadio> radios = [];
private readonly List<OperatingPosition> positions = [];
@@ -30,6 +35,9 @@ public sealed class AppSession : IDisposable
/// What was last said to the other stations about where this one is, so it
/// is said again only when it changes.
private (Frequency Where, string Mode, bool Running, int Radio) announced;
/// When the last `STATUS` went out. It is not sent every second: it carries
/// the rates, which are read out of the log.
private DateTime statusSent;
private DispatcherTimer? announcing;
private MessageSender? keyer;
private AlternatingCq? alternating;
@@ -310,8 +318,13 @@ public sealed class AppSession : IDisposable
// changed, rather than announced from each of them
announcing?.Stop();
announced = default;
statusSent = default;
announcing = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
announcing.Tick += (_, _) => AnnounceWhereIAm();
announcing.Tick += (_, _) =>
{
AnnounceWhereIAm();
SayHowIAmDoing();
};
announcing.Start();
Changed?.Invoke(this, EventArgs.Empty);
}
@@ -335,6 +348,35 @@ public sealed class AppSession : IDisposable
_ = link.SendBandAsync(position.Frequency, position.Mode, position.IsRunning, position.RadioNumber);
}
/// The `STATUS` message: everything the other operators' network status
/// windows show about this station, whether anything has changed or not. It
/// goes out on N1MM's own interval, and the rates in it are read out of the
/// log each time.
private void SayHowIAmDoing()
{
if (link is null || Position is not { } position
|| DateTime.UtcNow - statusSent < StatusInterval)
{
return;
}
statusSent = DateTime.UtcNow;
TimeSpan off = Logging?.Contest.MinimumOffTime ?? TimeSpan.FromMinutes(30);
IReadOnlyList<Qso> qsos = Logging?.Log.Qsos ?? [];
_ = link.SendStatusAsync(new StationStatus(
link.Me.PassFrequency,
position.Frequency,
position.IsRunning,
link.Operator,
position.Mode,
Rates.PerHour(qsos, 10, DateTime.UtcNow, off),
Rates.PerHour(qsos, 100, DateTime.UtcNow, off),
Logging?.Instance.OperatorCategory ?? "",
Logging?.Instance.TransmitterCategory ?? "",
// the second radio of a two-radio position is N1MM's Run2, which is
// what its R/M column reads
position.RadioNumber == 2));
}
/// A station named by hand, written `RUN-PC@192.168.1.5` with `:port` on
/// the end when that station is not on the usual one. Null for anything
/// else, because a line the operator has half typed is not an address.