Three pieces on top of the link: **The network status window**, on the entry window's Window menu. A row per station, this computer included the way N1MM shows it: number, address, operator, band, mode, run, transmit, pass frequency, the last message type, how long ago it arrived, the counts each way and the echo round trip. A station that broadcast the wrong version is named under the table in red — that is the one fault where everything looks connected and nothing arrives. The box at the bottom sends a line of chat, and Echo asks every station whether it is there. **Where this station is** goes out once a second when it has changed. The frequency, the mode and run have a dozen places they can change from — the radio moving, a band button, a QSY typed into the callsign box — so it is read and compared rather than announced from each of them. **The ten-minute rule.** `BandChangeRules` already counted changes and the stay on a band, and the entry window already showed the countdown, but only a user-defined contest carried a rule, so every built-in contest allowed anything. `ForCategory` gives a multi-operator entry with one or two transmitters a ten-minute stay, which is N1MM's fallback in `ContestInstance.BandChangeTimerDuration` for every contest that does not name its own. Its per-contest table is a few hundred cases in a decompiled hash switch and is not repeated. Config ▸ Edit Networked-Computer Names now covers both networks: the 12060 broadcast to other programs and the 12070 link, with the station number, the port, the version to claim and stations named by address for a network where a broadcast does not reach. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
134 lines
5.5 KiB
C#
134 lines
5.5 KiB
C#
using Nonemm.Core;
|
|
|
|
namespace Nonemm.Contests;
|
|
|
|
/// What a contest allows a station in the way of band changes, and how to
|
|
/// count the ones already made. N1MM shows the same count in its info window.
|
|
///
|
|
/// `Max` is how many changes the entry is allowed. The window the count is
|
|
/// taken over is the clock hour, unless `PerContest` counts the whole contest
|
|
/// or `PerPeriod` counts the hour up to now.
|
|
///
|
|
/// `MinimumStay` is how long the station has to stay on a band it has just
|
|
/// moved to. Both the count and the stay are worked out from the log, as
|
|
/// N1MM's are: the contact that lands on another band is the change.
|
|
public sealed record BandChangeRules(
|
|
int Max,
|
|
bool PerContest = false,
|
|
bool PerPeriod = false,
|
|
bool CountsModeChanges = false,
|
|
TimeSpan MinimumStay = default)
|
|
{
|
|
/// A contest that does not limit band changes at all.
|
|
public static readonly BandChangeRules None = new(0);
|
|
|
|
/// How long a multi-operator entry with one transmitter has to stay on a
|
|
/// band. It is the ten-minute rule, and it is N1MM's fallback for every
|
|
/// contest that does not name a number of its own.
|
|
public static readonly TimeSpan MultiOneStay = TimeSpan.FromMinutes(10);
|
|
|
|
/// What a contest allows an entry in this category, before the contest has
|
|
/// its say. These are N1MM's defaults, from
|
|
/// `ContestInstance.BandChangeTimerDuration` and `BandChangeCountMax`:
|
|
///
|
|
/// | Category | Changes counted | Stay |
|
|
/// |---|---|---|
|
|
/// | single operator | no | none |
|
|
/// | multi-operator, one transmitter | no | ten minutes |
|
|
/// | multi-operator, two transmitters | no | ten minutes |
|
|
/// | multi-operator, more | no | none |
|
|
///
|
|
/// The count is off by default because N1MM's own fallback for it is zero:
|
|
/// the contests that cap band changes per hour name the cap themselves, and
|
|
/// a contest class that does so overrides `Contest.BandChangesFor`. The
|
|
/// stay is the other way round — ten minutes is the fallback, and a contest
|
|
/// with no rule of its own gets it.
|
|
///
|
|
/// A station with a transmitter per band changes band by moving to another
|
|
/// radio, so nothing is counted for it.
|
|
public static BandChangeRules ForCategory(ContestEntry entry)
|
|
{
|
|
if (!entry.OperatorCategory.StartsWith("MULTI", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return None;
|
|
}
|
|
// N1MM reads the number of transmitters off the operator category on
|
|
// Cabrillo 2.0 and off the transmitter category after it. Both are
|
|
// checked, so an entry written either way is read
|
|
bool one = Is(entry, "MULTI-ONE", "ONE");
|
|
bool two = Is(entry, "MULTI-TWO", "TWO");
|
|
return one || two ? None with { MinimumStay = MultiOneStay } : None;
|
|
}
|
|
|
|
private static bool Is(ContestEntry entry, string operatorCategory, string transmitterCategory) =>
|
|
entry.OperatorCategory.Equals(operatorCategory, StringComparison.OrdinalIgnoreCase)
|
|
|| entry.TransmitterCategory.Equals(transmitterCategory, StringComparison.OrdinalIgnoreCase);
|
|
|
|
public bool IsCounted => Max > 0;
|
|
|
|
/// True for a contest that says how long a station has to stay on a band.
|
|
public bool HasStayTimer => MinimumStay > TimeSpan.Zero;
|
|
|
|
/// How many band changes fall in the window that ends at `now`. The first
|
|
/// contact of the log is not a change.
|
|
public int CountedIn(IEnumerable<Qso> qsos, DateTime now)
|
|
{
|
|
List<Qso> ordered = qsos.OrderBy(q => q.TimestampUtc).ToList();
|
|
int changes = 0;
|
|
for (int at = 1; at < ordered.Count; at++)
|
|
{
|
|
if (IsChange(ordered[at - 1], ordered[at]) && IsInWindow(ordered[at].TimestampUtc, now))
|
|
{
|
|
changes++;
|
|
}
|
|
}
|
|
return changes;
|
|
}
|
|
|
|
/// How much of the stay on this band is left. Zero once the time is up,
|
|
/// and zero for a log that has not changed band yet.
|
|
///
|
|
/// The stay is counted from the minute after the contact that changed
|
|
/// band, which is where N1MM starts it: a contact at 12:00:40 puts the
|
|
/// station on the band from 12:01.
|
|
public TimeSpan StayLeft(IEnumerable<Qso> qsos, DateTime now)
|
|
{
|
|
if (!HasStayTimer || LastChange(qsos) is not { } changed)
|
|
{
|
|
return TimeSpan.Zero;
|
|
}
|
|
TimeSpan left = MinimumStay - (now - NextMinute(changed));
|
|
return left > TimeSpan.Zero ? left : TimeSpan.Zero;
|
|
}
|
|
|
|
/// When the last contact that moved the station to another band was, or
|
|
/// null while every contact has been on one band.
|
|
private DateTime? LastChange(IEnumerable<Qso> qsos)
|
|
{
|
|
List<Qso> ordered = qsos.OrderBy(q => q.TimestampUtc).ToList();
|
|
DateTime? found = null;
|
|
for (int at = 1; at < ordered.Count; at++)
|
|
{
|
|
if (IsChange(ordered[at - 1], ordered[at]))
|
|
{
|
|
found = ordered[at].TimestampUtc;
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
private static DateTime NextMinute(DateTime when) =>
|
|
new DateTime(when.Year, when.Month, when.Day, when.Hour, when.Minute, 0, when.Kind)
|
|
.AddMinutes(1);
|
|
|
|
private bool IsChange(Qso before, Qso after) =>
|
|
before.Band != after.Band
|
|
|| (CountsModeChanges && before.Mode.Category != after.Mode.Category);
|
|
|
|
private bool IsInWindow(DateTime when, DateTime now) =>
|
|
PerContest
|
|
|| (PerPeriod
|
|
? when <= now && now - when < TimeSpan.FromHours(1)
|
|
: when.Date == now.Date && when.Hour == now.Hour);
|
|
}
|