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 @@ - - - - -