From 0b92bfc6eea373ccdd67f61e86fccc25a22cf0a4 Mon Sep 17 00:00:00 2001 From: ericek111 Date: Thu, 3 Sep 2026 23:15:44 +0000 Subject: [PATCH] Fill in the rest of N1MM's network status window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seven tabs now, in N1MM's order, and the messages behind them. **Options** holds what there is something behind: Log QSOs at all Stations, which decides whether the contacts the others log are kept here; the message trace, moved off the Messages tab where N1MM does not keep it; and Run1/Run2, which used to be guessed from the radio number and is now the operator's choice, sent in every `STATUS` and every `XMIT`. The transmit interlocks stay out: this program does not block a transmitter, and a station that claimed it did would be waited on for nothing. **Call Stack Targets.** A callsign heard at one station and handed to the position that will work it — `STACKANOTHERCALL` with the call alone, `STACKCALL` with the contact behind it. The target is picked from the tab or from the station list, and what arrives goes on the call stack of the radio the operator is on, sorted by whether it is a multiplier here. **The master** is the station the entry follows for cluster spots and the time. `MASTER` names it to everybody, so the column agrees everywhere. **The clock.** `TIME` carries a station's clock and Force Time Sync sends it. N1MM sets the clock from it when it is running as an administrator and says how far out it is when it is not; this program always does the second, because setting the clock needs root and a contest logger has no business asking for it. Anything over two seconds is called out. **Serial numbers.** Reset Serial Numbers at all Stations sends `RESETQSONRS` and every station starts again at one. The contacts already logged keep the numbers they were sent with. **What the entry agrees on** is now checked rather than assumed: | What | How | |---|---| | the contest | `CONTESTNAME` as soon as a connection opens, and again in every `CHECKSUM` | | the country file | its issue date — the publisher's `=VER20260826` — rides in `STATUS` | | the computer name | longer than N1MM's fifteen characters is named at startup; a second computer answering to this one's name is named when its beacon arrives | | the station number | two stations on the same number are named when the second says so | `CountryFile` reads its issue date, which nothing had asked it for before. Left for later: the master relaying its cluster spots, and the shared serial-number pool where stations claim numbers in advance. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU --- docs/unfinished.md | 51 ++++- src/Nonemm.App/AppSession.cs | 72 +++++- src/Nonemm.App/Configuration/Settings.cs | 5 + src/Nonemm.App/SessionContactLog.cs | 4 + .../Windows/NetworkStatusWindow.axaml | 79 +++++-- .../Windows/NetworkStatusWindow.axaml.cs | 121 +++++++++- src/Nonemm.App/Windows/StationRow.cs | 3 + src/Nonemm.Core/Country/CountryFile.cs | 22 +- src/Nonemm.Network/ContactLog.cs | 8 + src/Nonemm.Network/NetworkedStation.cs | 19 ++ src/Nonemm.Network/StackedCall.cs | 11 + src/Nonemm.Network/StationLink.cs | 212 ++++++++++++++++++ src/Nonemm.Network/StationMessages.cs | 49 +++- src/Nonemm.Network/StationStatus.cs | 5 +- src/Nonemm.Session/ContestSession.cs | 15 ++ .../Country/CountryFileTests.cs | 13 ++ .../Nonemm.Network.Tests/StationLinkTests.cs | 173 +++++++++++++- .../NetworkedContactsTests.cs | 16 ++ 18 files changed, 838 insertions(+), 40 deletions(-) create mode 100644 src/Nonemm.Network/StackedCall.cs diff --git a/docs/unfinished.md b/docs/unfinished.md index 1456b83..21bb1f1 100644 --- a/docs/unfinished.md +++ b/docs/unfinished.md @@ -396,16 +396,57 @@ burst of three does not read as thousands an hour. The five transmit-interlock flags in the message are sent as false: this program does not block a transmitter. -**The network status window** has N1MM's Stations, Actions, Resynchronize, -Rescore and Messages tabs and +**The network status window** has all seven of N1MM's tabs — Stations, +Resynchronize, Rescore, Options, Call Stack Targets, Actions and Messages — and its columns — Computer, IP Address, Pass, Run, 10, 100, Freq, Op, Msg, Send, Receive, Band, R/M, Mode. Right-click a row to pass the contact being worked to that station, to talk, or to close and open its connection; a connection closed by hand stays closed until it is opened again, because the next beacon would otherwise open it within ten seconds. The Resynchronize and Rescore tabs work -on a period — the contest, since a time, or the last few hours. Left out: the -transmit interlocks of N1MM's Options tab, the Master column and the -call-stacking target tab. There is nothing behind any of them here. +on a period — the contest, since a time, or the last few hours. Options holds +what there is something behind: Log QSOs at all Stations, the message trace, and +Run1/Run2. Left out are the transmit interlocks — this program does not block a +transmitter, and a station that claimed it did would be waited on for nothing — +and the two telnet options, which wait on the master relaying cluster spots. + +**What the entry agrees on.** Four things have to match across the stations of +an entry, and each is now checked rather than assumed: + +| What | How it is checked | +|---|---| +| the program version | the beacon, which N1MM refuses to talk past | +| the contest | `CONTESTNAME` as soon as a connection opens, and again in every `CHECKSUM` | +| the callsign | `CHECKSUM` | +| the country file | its issue date rides in `STATUS`; two issues score the same contact differently | + +A computer name longer than the fifteen characters N1MM allows is named at +startup, a second computer answering to this one's name is named when its +beacon arrives, and two stations that have taken the same station number are +named when the second one says so. + +**The clock.** `TIME` carries a station's clock and Force Time Sync sends it. +N1MM sets the clock from it when it is running as an administrator and says how +far out it is when it is not; this program always does the second, because +setting the clock needs root and a contest logger has no business asking for it. +Anything over two seconds is called out. + +**The master** is the station the entry follows for cluster spots and the time, +set from the station list and told to everybody, so the `Master` column agrees +everywhere. What it does not do yet is relay: N1MM's master passes its cluster +spots to the others over the link, and `ADDSPOT` and `PACKET` are not written. + +**Call stacking.** A callsign heard at one station and handed to the position +that will work it: `STACKANOTHERCALL` with the call alone, `STACKCALL` with the +contact behind it. The target is picked from the Call Stack Targets tab or from +the station list, and what arrives goes on the call stack of the radio the +operator is on, sorted by whether it is a multiplier here. + +**Serial numbers.** Reset Serial Numbers at all Stations sends `RESETQSONRS` +and every station starts again at one. The contacts already logged keep the +numbers they were sent with; the reset lasts as long as the program is open, +the way N1MM's does. The shared number pool — `RESERVENR`, `QSONRS`, `REJECTNR`, +where stations claim numbers in advance so two positions cannot send the same +one — is not written. **The ten-minute rule.** `BandChangeRules` counts band changes and the stay on a band, and the entry window shows the countdown. What was missing was the rule diff --git a/src/Nonemm.App/AppSession.cs b/src/Nonemm.App/AppSession.cs index 7838879..2779cb8 100644 --- a/src/Nonemm.App/AppSession.cs +++ b/src/Nonemm.App/AppSession.cs @@ -231,6 +231,9 @@ public sealed class AppSession : IDisposable positions.Add(new OperatingPosition(Logging, number)); } activeRadio = Math.Min(activeRadio, positions.Count - 1); + // the other stations refuse to resynchronise with a station on another + // contest, and say so, which is only any use if they are told + _ = link?.SendContestNameAsync(); Logging.Changed += (_, _) => Changed?.Invoke(this, EventArgs.Empty); Logging.Logged += (_, qso) => { @@ -319,6 +322,10 @@ public sealed class AppSession : IDisposable link.Log = new SessionContactLog(this); link.RescoreRequested += (_, _) => Dispatcher.UIThread.Post(() => Rescore(DateTime.MinValue)); + link.SerialNumbersReset += (_, _) => + Dispatcher.UIThread.Post(ResetSerialNumbers); + link.CallStacked += (_, stacked) => + Dispatcher.UIThread.Post(() => TakeStackedCall(stacked)); foreach (string peer in Settings.StationLinkPeers) { if (Peer(peer) is var (name, address, port)) @@ -372,6 +379,62 @@ public sealed class AppSession : IDisposable statusSent = default; } + /// True while this station is the second run radio of a two-radio + /// position, which is N1MM's Run1/Run2 switch on the network status window. + /// It goes out in every `STATUS` and is what the other stations read in + /// their `R/M` column. + public bool IsRun2 + { + get; + set + { + field = value; + statusSent = default; + Changed?.Invoke(this, EventArgs.Empty); + } + } + + /// Starts the serial numbers again at one. The contacts already logged keep + /// the numbers they were sent with; it is the next number that changes. + public void ResetSerialNumbers() + { + if (Logging is not null) + { + Logging.SerialNumbersFrom = DateTime.UtcNow; + Changed?.Invoke(this, EventArgs.Empty); + } + } + + /// A callsign another station handed to this one. It goes on the call stack + /// of the radio the operator is on, which is where the entry window takes + /// them from. + private void TakeStackedCall(StackedCall stacked) + { + if (Position is not { } position || stacked.Call.Length == 0) + { + return; + } + position.Stack.Add(stacked.Call, IsNewMultiplier(stacked)); + Changed?.Invoke(this, EventArgs.Empty); + } + + /// Whether a stacked call is worth a multiplier here, which is what decides + /// where it goes in the stack. A call with no contact behind it is judged + /// on the callsign alone. + private bool IsNewMultiplier(StackedCall stacked) => + Logging is not null + && (stacked.Contact is { } contact + ? Logging.Log.Judge(contact).NewMultipliers.Count > 0 + : Logging.Log.Judge(new Qso + { + Id = "", + TimestampUtc = DateTime.UtcNow, + Call = Callsign.Parse(stacked.Call), + Frequency = Position?.Frequency ?? Frequency.Zero, + Mode = Position?.Mode ?? Modes.Cw, + ContestName = Logging.Contest.Name, + }).NewMultipliers.Count > 0); + /// Scores the log again and writes the points and multiplier flags from /// `fromUtc` on back to the database. Returns how many rows were written. /// Another station asks for this at the end of a resync, and the rescore @@ -402,9 +465,8 @@ public sealed class AppSession : IDisposable 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)); + Countries?.Version ?? "", + IsRun2)); } /// A station named by hand, written `RUN-PC@192.168.1.5` with `:port` on @@ -433,6 +495,10 @@ public sealed class AppSession : IDisposable { return; } + if (!Settings.LogQsosAtAllStations && update is not ContactsCleared) + { + return; + } switch (update) { case ContactLogged logged: diff --git a/src/Nonemm.App/Configuration/Settings.cs b/src/Nonemm.App/Configuration/Settings.cs index 3def8cf..67d05f4 100644 --- a/src/Nonemm.App/Configuration/Settings.cs +++ b/src/Nonemm.App/Configuration/Settings.cs @@ -142,6 +142,11 @@ public sealed record Settings /// with `:port` on the end when that station is not on the usual port. public IReadOnlyList StationLinkPeers { get; init; } = []; + /// Whether the contacts the other stations log are kept here. N1MM calls it + /// Log QSOs at all Stations and every station of an entry normally has it + /// on: a log per station is a log the sponsor has to be sent in pieces. + public bool LogQsosAtAllStations { get; init; } = true; + /// The call history file for this contest, or empty for none. They are /// published per contest, so this is not a fixed name. public string CallHistoryFile { get; init; } = ""; diff --git a/src/Nonemm.App/SessionContactLog.cs b/src/Nonemm.App/SessionContactLog.cs index d957efb..83710ce 100644 --- a/src/Nonemm.App/SessionContactLog.cs +++ b/src/Nonemm.App/SessionContactLog.cs @@ -21,6 +21,10 @@ internal sealed class SessionContactLog : ContactLog public string MyCall => Ask(() => session.Settings.Station.Callsign); + public string ContestSubType => Ask(() => session.Logging?.Instance.SubType ?? ""); + + public string CountryFileVersion => Ask(() => session.Countries?.Version ?? ""); + public IReadOnlyList Contacts(string computerName, DateTime fromUtc, TimeSpan length) => Ask(() => session.Logging is not { } logging ? [] diff --git a/src/Nonemm.App/Windows/NetworkStatusWindow.axaml b/src/Nonemm.App/Windows/NetworkStatusWindow.axaml index 61b1328..2fe0a90 100644 --- a/src/Nonemm.App/Windows/NetworkStatusWindow.axaml +++ b/src/Nonemm.App/Windows/NetworkStatusWindow.axaml @@ -2,13 +2,13 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:Nonemm.App.Windows" x:Class="Nonemm.App.Windows.NetworkStatusWindow" - Title="Network Status" Width="900" Height="360"> + Title="Network Status" Width="1000" Height="380"> - + @@ -25,9 +25,11 @@ + + @@ -45,6 +47,7 @@ + @@ -52,24 +55,7 @@ - - - - -