diff --git a/docs/unfinished.md b/docs/unfinished.md index 7d2b960..afa9e8a 100644 --- a/docs/unfinished.md +++ b/docs/unfinished.md @@ -88,6 +88,13 @@ character away from the spotted one is looked up in the callsign database, which is a few hundred lookups per spot; two characters away is a few hundred thousand, which is too much work per spot for the little it would add. +**Two BARTG contests share one set of rules.** N1MM has a class each for the +March HF RTTY contest, `BARTGSRTTY`, and the September sprint, `BARTGRTTYS`. +This program has one contest, named `BARTGRTTYS`, holding the March rules — the +ones the station's log needs — and both of N1MM's names open a log with it. A +sprint log would be scored by the wrong rules, and a log started here writes +`BARTGRTTYS` into the contest name, which N1MM would read as the sprint. + **Digital modes.** A contact can be logged as RTTY or another digital mode, and the contest rules score it, but there is no digital window: no decoding, no transmitting, no interface to fldigi, MMTTY or similar. @@ -160,9 +167,6 @@ What still differs, and why: `MultWindowType` setting, and those lists are not held here, so a US state sent into an OK/OM DX log counts as a multiplier here and not in N1MM. Three contacts across the two logs. -- **The BARTG Sprint** stores the time of the contact in the exchange column, - which N1MM fills in by itself. Nothing here fills it in yet, so a log written - here carries the serial number alone. ## Known rough edges diff --git a/src/Nonemm.App/Windows/EntryWindow.Macros.cs b/src/Nonemm.App/Windows/EntryWindow.Macros.cs index 9ec2f56..4b32230 100644 --- a/src/Nonemm.App/Windows/EntryWindow.Macros.cs +++ b/src/Nonemm.App/Windows/EntryWindow.Macros.cs @@ -79,6 +79,11 @@ public sealed partial class EntryWindow Status("no keyer — Config ▸ Keyer"); return; } + if (index == Esm.Exchange) + { + // the BARTG contest sends the time, and logs the one it sent + Logging.StampSentTime(); + } string text = prefix + plan.Text; // the box has to point at this radio before the key does await session.PointTransmitAtAsync(radioNumber); diff --git a/src/Nonemm.Contests/Contest.cs b/src/Nonemm.Contests/Contest.cs index 0ee909a..8f6d20a 100644 --- a/src/Nonemm.Contests/Contest.cs +++ b/src/Nonemm.Contests/Contest.cs @@ -55,6 +55,11 @@ public interface Contest /// contests the same question. bool UsesItuZones => false; + /// True for a contest that sends the time of the exchange as part of it. + /// The time this station sent goes in the `MiscText` column, stamped as + /// the message goes out. Only the BARTG RTTY contest asks for it. + bool StampsSentTime => false; + /// True for a contest whose log carries QTC traffic as rows of its own. /// The log window then shows the series they belong to. bool HasQtcTraffic => false; diff --git a/src/Nonemm.Contests/Rules/BartgSprint.cs b/src/Nonemm.Contests/Rules/BartgSprint.cs index de23696..d31ad4f 100644 --- a/src/Nonemm.Contests/Rules/BartgSprint.cs +++ b/src/Nonemm.Contests/Rules/BartgSprint.cs @@ -2,12 +2,19 @@ using Nonemm.Core; namespace Nonemm.Contests.Rules; -/// The BARTG RTTY Sprint. Every contact is one point; the multipliers are the +/// The BARTG RTTY contest. Every contact is one point. The multipliers are the /// countries and the US and Canadian call areas once per band, and the -/// continents once in the contest. +/// continents once in the contest; the score is the points times the countries +/// and areas together, times the continents. /// -/// The exchange is a serial number. N1MM also writes the time of the contact -/// into the exchange column, which the Cabrillo line carries. +/// The exchange is a report, a serial number and the time the exchange was +/// sent. The time this station sends is stamped into the `MiscText` column as +/// the message goes out, which is the column N1MM keeps it in. +/// +/// The rules are N1MM's `CBARTGSpring`, the March HF contest the station has +/// logs of. N1MM's September sprint is a class of its own with a shorter +/// exchange, and both of N1MM's names for these two contests open a log here +/// with the rules below. public sealed class BartgSprint : Contest { public string Name => "BARTGRTTYS"; @@ -18,9 +25,15 @@ public sealed class BartgSprint : Contest public IReadOnlyList ExchangeFieldsFor(StationInfo me) => [ + new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number), + new ExchangeField("Time", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 4 }, ]; + /// The time this station sent goes in the `MiscText` column, as it does in + /// N1MM. + public bool StampsSentTime => true; + public IReadOnlyList MultiplierNames => ["Countries", "Areas", "Continents"]; public DupeScope DupeScope => DupeScope.PerBand; @@ -54,18 +67,34 @@ public sealed class BartgSprint : Contest } public int TotalScore(ScoreTally tally, ContestEntry entry) => - tally.Points * tally.TotalMultipliers; + tally.Points + * (tally.MultiplierCount(1) + tally.MultiplierCount(2)) + * Math.Max(1, tally.MultiplierCount(3)); public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me, ContestEntry entry) => new( [ - new CabrilloField(me.Callsign, 13), - new CabrilloField($"{qso.SentNumber:000}", 4), - new CabrilloField(qso.TimestampUtc.ToString("HHmm"), 4), + new CabrilloField(me.Callsign, 10), + new CabrilloField(qso.SentReport, 3), + new CabrilloField($"{qso.SentNumber:0000}", 4), + new CabrilloField(SentTime(qso), 4), ], [ - new CabrilloField(qso.Call.Text, 13), - new CabrilloField($"{qso.ReceivedNumber:000}", 4), - new CabrilloField(qso.Exchange1, 4), + new CabrilloField(qso.Call.Text, 10), + new CabrilloField(qso.ReceivedReport, 3), + new CabrilloField($"{qso.ReceivedNumber:0000}", 4), + new CabrilloField(FourDigits(qso.Exchange1), 4), ]); + + /// The time this station sent, and the time of the contact for a contact + /// logged before that was filled in. N1MM writes the same two. + private static string SentTime(Qso qso) => + qso.MiscText.Trim().Length > 0 + ? FourDigits(qso.MiscText) + : qso.TimestampUtc.ToString("HHmm"); + + /// `905` is written `0905`, which is what N1MM writes for a time typed + /// without its leading zero. + private static string FourDigits(string time) => + int.TryParse(time.Trim(), out int minutes) ? $"{minutes:0000}" : time.Trim(); } diff --git a/src/Nonemm.Session/OperatingPosition.cs b/src/Nonemm.Session/OperatingPosition.cs index 720e63a..a205523 100644 --- a/src/Nonemm.Session/OperatingPosition.cs +++ b/src/Nonemm.Session/OperatingPosition.cs @@ -44,6 +44,10 @@ public sealed class OperatingPosition public string Comment { get; set; } = ""; + /// The time this station sent, `HHmm`, for a contest that exchanges it. + /// Empty until the exchange has gone out. + public string SentTime { get; private set; } = ""; + public Contest Contest => Session.Contest; public ContestInstance Instance => Session.Instance; @@ -199,13 +203,28 @@ public sealed class OperatingPosition { throw new InvalidOperationException($"the exchange for {Entry.Call} is not complete"); } + StampSentTime(); Qso stored = Session.Add(Noted(BuildQso(Entry.Call.Trim()))); Entry.Clear(); OtherName = ""; Comment = ""; + SentTime = ""; return stored; } + /// Notes the time the exchange went out, which the BARTG RTTY contest + /// sends and logs. The first message of a contact sets it, so the time in + /// the log is the time the other station copied, not the time the contact + /// was logged. Does nothing for the contests that do not send a time. + public void StampSentTime() + { + if (Contest.StampsSentTime && SentTime.Length == 0 && Entry.Call.Trim().Length > 2) + { + SentTime = DateTime.UtcNow.ToString("HHmm"); + Session.NotifyChanged(); + } + } + /// Puts the report the operator would send anyway into the report boxes /// that are empty: 599 on CW and digital modes, 59 on phone. N1MM does the /// same when the callsign box is left, which is why neither program stops @@ -386,6 +405,7 @@ public sealed class OperatingPosition Entry.Clear(); OtherName = ""; Comment = ""; + SentTime = ""; Session.NotifyChanged(); } @@ -396,6 +416,7 @@ public sealed class OperatingPosition { Name = qso.Name.Length > 0 ? qso.Name : OtherName.Trim(), Comment = qso.Comment.Length > 0 ? qso.Comment : Comment.Trim(), + MiscText = qso.MiscText.Length > 0 ? qso.MiscText : SentTime, }; /// The report an operator sends without thinking about it: 599 on CW and diff --git a/tests/Nonemm.Formats.Tests/CabrilloWriterTests.cs b/tests/Nonemm.Formats.Tests/CabrilloWriterTests.cs index 9b27333..e357b35 100644 --- a/tests/Nonemm.Formats.Tests/CabrilloWriterTests.cs +++ b/tests/Nonemm.Formats.Tests/CabrilloWriterTests.cs @@ -120,6 +120,33 @@ public class CabrilloWriterTests Assert.EndsWith("ABC ", writer.QsoLine(Contact() with { Exchange1 = "ABCDEF" })); } + /// BARTG writes the time this station sent, which is the `MiscText` + /// column, and the time the other station sent in the exchange column. + [Fact] + public void TheBartgLineCarriesBothTimes() + { + CabrilloWriter writer = new(new BartgSprint(), Me); + + string line = writer.QsoLine( + Contact(14_025, Modes.Rtty) with { MiscText = "1704", Exchange1 = "905" }); + + Assert.Equal( + "QSO: 14025 RY 2026-05-30 1234 " + + "DL1ABC 599 0012 1704 " + + "JA1XYZ 599 0034 0905 0", + line); + } + + /// A contact logged before the time was stamped falls back to the time of + /// the contact, as N1MM does. + [Fact] + public void TheBartgLineFallsBackToTheTimeOfTheContact() + { + CabrilloWriter writer = new(new BartgSprint(), Me); + + Assert.Contains("1234 JA1XYZ", writer.QsoLine(Contact(14_025, Modes.Rtty))); + } + /// `CabrilloFormat = 2` is the NAQP line: the serial number and the two /// exchange values, each in a column of its own. [Fact] diff --git a/tests/Nonemm.Session.Tests/OperatingPositionTests.cs b/tests/Nonemm.Session.Tests/OperatingPositionTests.cs index ce95c4d..0644c51 100644 --- a/tests/Nonemm.Session.Tests/OperatingPositionTests.cs +++ b/tests/Nonemm.Session.Tests/OperatingPositionTests.cs @@ -50,6 +50,38 @@ public class OperatingPositionTests session.Entry.Set(ExchangeSlot.Zone, zone); } + /// The BARTG contest sends the time of the exchange, and logs the time it + /// sent rather than the time the contact was logged. + [Fact] + public void TheTimeTheExchangeWentOutIsLoggedForBartg() + { + OperatingPosition session = Session(new BartgSprint()); + session.Entry.Call = "JA1XYZ"; + session.Entry.Set(ExchangeSlot.ReceivedReport, "599"); + session.Entry.Set(ExchangeSlot.SerialNumber, "12"); + session.Entry.Set(ExchangeSlot.Exchange1, "1646"); + session.StampSentTime(); + string stamped = session.SentTime; + + Qso logged = session.LogContact(); + + Assert.Equal(4, stamped.Length); + Assert.Equal(stamped, logged.MiscText); + Assert.Equal("", session.SentTime); + } + + /// A contest that does not exchange a time keeps the column empty. + [Fact] + public void NothingIsStampedForAContestThatSendsNoTime() + { + OperatingPosition session = Session(); + Type(session, "JA1XYZ", "25"); + session.StampSentTime(); + + Assert.Equal("", session.SentTime); + Assert.Equal("", session.LogContact().MiscText); + } + [Fact] public void LoggingAContactPutsItInTheLogAndClearsTheEntry() {