Take serial numbers from a pool the whole entry shares

Each station worked out its next number as the highest in its log plus one,
so two operators typing at the same moment both sent the same number and
one of the two contacts was wrong in the sponsor's log.

SerialNumberPool holds the highest number taken per band, which is N1MM's
QSONumbers array. A number is taken at the first character of the callsign,
as N1MM takes it, and ReserveNr tells the other stations before the contact
is worked. A number at or below one this station has used is answered with
the whole pool, N1MM's QSONRS, and the station that is behind counts on from
there. The pool also goes out when a contest is opened and when the numbers
are started again.

Contest.NumbersByBand says whether the numbers run per band or as one
sequence for the station. CQ WPX runs one per band for a multi-two and a
multi-multi; every other contest here runs one sequence. The band index in
the message is N1MM's, so the array lines up with theirs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
This commit is contained in:
2026-09-10 11:56:49 +00:00
co-authored by Claude Opus 5
parent 33c7631e17
commit 7ba0b44fc1
21 changed files with 677 additions and 40 deletions
+35
View File
@@ -234,6 +234,10 @@ public sealed class AppSession : IDisposable
// 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();
// and where this station's serial numbers have got to, so a station
// joining a contest already under way counts from the same place.
// N1MM sends the pool at the same point
_ = link?.SendSerialNumbersAsync(Logging.Numbers.Snapshot());
Logging.Changed += (_, _) => Changed?.Invoke(this, EventArgs.Empty);
Logging.Logged += (_, qso) =>
{
@@ -250,6 +254,10 @@ public sealed class AppSession : IDisposable
// and the same three to the other logging computers, which is a
// different protocol on a different port
Logging.Logged += (_, qso) => _ = link?.SendLoggedAsync(qso);
// the number is announced when it is taken rather than when the contact
// is logged, which is what keeps two operators off the same number
Logging.Numbers.Reserved += (_, taken) =>
_ = link?.SendReservedNumberAsync(taken.Number, SomewhereOn(taken.Band));
Logging.Edited += (_, change) =>
_ = link?.SendEditedAsync(change.Qso, change.OldCall, change.OldTimestampUtc);
Logging.Deleted += (_, qso) => _ = link?.SendDeletedAsync(qso);
@@ -259,6 +267,11 @@ public sealed class AppSession : IDisposable
ContestChanged?.Invoke(this, EventArgs.Empty);
}
/// A frequency on the band a number was taken on. The message carries a
/// frequency because N1MM's does, and both ends read nothing off it but the
/// band.
private static Frequency SomewhereOn(Band? band) => band?.Low ?? Frequency.Zero;
/// One entry position per radio, and one when there is no radio at all.
private int PositionCount => Math.Max(1, Settings.Radios.Count(r => r.IsEnabled));
@@ -326,6 +339,10 @@ public sealed class AppSession : IDisposable
Dispatcher.UIThread.Post(ResetSerialNumbers);
link.CallStacked += (_, stacked) =>
Dispatcher.UIThread.Post(() => TakeStackedCall(stacked));
link.NumberReserved += (_, taken) =>
Dispatcher.UIThread.Post(() => TakeReservedNumber(taken));
link.SerialNumbersArrived += (_, numbers) =>
Dispatcher.UIThread.Post(() => Logging?.Numbers.Merge(numbers));
foreach (string peer in Settings.StationLinkPeers)
{
if (Peer(peer) is var (name, address, port))
@@ -401,10 +418,28 @@ public sealed class AppSession : IDisposable
if (Logging is not null)
{
Logging.SerialNumbersFrom = DateTime.UtcNow;
_ = link?.SendSerialNumbersAsync(Logging.Numbers.Snapshot());
Changed?.Invoke(this, EventArgs.Empty);
}
}
/// A serial number another station has taken. It is noted here so this
/// station counts past it. A number at or below what this station has
/// already used says the other one is behind, and the whole pool goes back
/// so it can catch up, which is what N1MM answers with.
private void TakeReservedNumber(ReservedNumberMessage taken)
{
if (Logging is not { } logging)
{
return;
}
if (!logging.Numbers.Note(taken.Number, Bands.ForFrequency(taken.Frequency)))
{
_ = link?.SendSerialNumbersAsync(logging.Numbers.Snapshot());
}
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.