Two entry windows and an SO2R box
LoggingSession mixed two things: the contest, which one station has one of, and what the operator is typing, which belongs to a radio. It is now ContestSession and RadioPosition. Two positions share one log, one score and one run of serial numbers, so a station worked on radio 1 is a dupe on radio 2. Frequency, mode, split, what is typed and whether you are running belong to each radio on its own. A second radio opens a second entry window. It has no menu of its own: it is another view of the same contest, not another program. Only the window for radio 1 opens the database and the connections. Ctrl+Tab moves the operator to the other radio and the keyboard follows. Ctrl+Shift+Tab puts both radios in the headphones. OtrspBox speaks OTRSP to an SO2R box over a serial port: TX1/TX2 for the key, RX1/RX2 and RX1S for the headphones, AUXnnn for an output line. It follows the active radio, and every message points the box at this radio before keying, so a message cannot go out of the radio the operator has just left. A command that would change nothing is not sent, because the box works relays; N1MM does the same. The box takes a Stream, so what it sends is tested without a serial port. Still missing: alternating CQ, and voice keying on the second radio. Looked at under Xvfb with two fake radios, one of them split: both windows up, radio 1 showing 14008.00 into 14020.0, radio 2 on 14030.00, Ctrl+Tab moving the keyboard between them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
298
tests/Nonemm.Session.Tests/RadioPositionTests.cs
Normal file
298
tests/Nonemm.Session.Tests/RadioPositionTests.cs
Normal file
@@ -0,0 +1,298 @@
|
||||
using Nonemm.Contests;
|
||||
using Nonemm.Contests.Rules;
|
||||
using Nonemm.Core;
|
||||
using Nonemm.Core.Country;
|
||||
using Nonemm.Storage;
|
||||
|
||||
namespace Nonemm.Session.Tests;
|
||||
|
||||
public class RadioPositionTests
|
||||
{
|
||||
private const string Countries = """
|
||||
Germany: 14: 28: EU: 51.00: -10.00: -1.0: DL:
|
||||
DL,DK,DJ;
|
||||
Japan: 25: 45: AS: 36.40: -138.38: -9.0: JA:
|
||||
JA,JH,JR;
|
||||
""";
|
||||
|
||||
private static readonly StationInfo Me = new()
|
||||
{
|
||||
Callsign = "DL1ABC",
|
||||
CqZone = 14,
|
||||
ItuZone = 28,
|
||||
Continent = "EU",
|
||||
CountryPrefix = "DL",
|
||||
};
|
||||
|
||||
private static RadioPosition Session(Contest? contest = null) =>
|
||||
new(Shared(new FakeLogStore(), contest));
|
||||
|
||||
private static ContestSession Shared(FakeLogStore store, Contest? contest = null)
|
||||
{
|
||||
ContestInstance instance = store.AddContest(new ContestInstance
|
||||
{
|
||||
ContestNumber = 0,
|
||||
ContestName = "CQWW",
|
||||
});
|
||||
return new ContestSession(
|
||||
store,
|
||||
contest ?? new CqWorldWide(ModeCategory.Cw),
|
||||
instance,
|
||||
Me,
|
||||
CountryFile.Parse(Countries));
|
||||
}
|
||||
|
||||
private static void Type(RadioPosition session, string call, string zone)
|
||||
{
|
||||
session.Entry.Call = call;
|
||||
session.Entry.Set(ExchangeSlot.ReceivedReport, "599");
|
||||
session.Entry.Set(ExchangeSlot.Zone, zone);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoggingAContactPutsItInTheLogAndClearsTheEntry()
|
||||
{
|
||||
RadioPosition session = Session();
|
||||
Type(session, "JA1XYZ", "25");
|
||||
Qso logged = session.LogContact();
|
||||
|
||||
Assert.Equal("JA1XYZ", logged.Call.Text);
|
||||
Assert.Equal(25, logged.Zone);
|
||||
Assert.Equal(3, logged.Points);
|
||||
Assert.Equal("", session.Entry.Call);
|
||||
Assert.Single(session.Log.Qsos);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CountryAndPrefixAreFilledInFromTheCountryFile()
|
||||
{
|
||||
RadioPosition session = Session();
|
||||
Type(session, "JA1XYZ", "25");
|
||||
Qso logged = session.LogContact();
|
||||
|
||||
Assert.Equal("JA", logged.CountryPrefix);
|
||||
Assert.Equal("AS", logged.Continent);
|
||||
Assert.Equal("JA1", logged.WpxPrefix);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnIncompleteExchangeIsNotLogged()
|
||||
{
|
||||
RadioPosition session = Session();
|
||||
session.Entry.Call = "JA1XYZ";
|
||||
Assert.Throws<InvalidOperationException>(() => session.LogContact());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheSameStationOnTheSameBandReadsAsADupe()
|
||||
{
|
||||
RadioPosition session = Session();
|
||||
Type(session, "JA1XYZ", "25");
|
||||
session.LogContact();
|
||||
Type(session, "JA1XYZ", "25");
|
||||
Assert.True(session.Verdict()?.IsDupe);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MovingBandClearsTheDupe()
|
||||
{
|
||||
RadioPosition session = Session();
|
||||
Type(session, "JA1XYZ", "25");
|
||||
session.LogContact();
|
||||
session.Tune(Frequency.FromKilohertz(7_025));
|
||||
Type(session, "JA1XYZ", "25");
|
||||
Assert.False(session.Verdict()?.IsDupe);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerialNumbersCountUp()
|
||||
{
|
||||
RadioPosition session = Session(new CqWpx(ModeCategory.Cw));
|
||||
Assert.Equal(1, session.SentNumber);
|
||||
session.Entry.Call = "JA1XYZ";
|
||||
session.Entry.Set(ExchangeSlot.ReceivedReport, "599");
|
||||
session.Entry.Set(ExchangeSlot.SerialNumber, "12");
|
||||
session.LogContact();
|
||||
Assert.Equal(2, session.SentNumber);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpaceMovesThroughTheBoxesAndBackToTheCall()
|
||||
{
|
||||
RadioPosition session = Session();
|
||||
Assert.Equal(0, session.Entry.Focus);
|
||||
session.Entry.Advance();
|
||||
Assert.Equal(1, session.Entry.Focus);
|
||||
session.Entry.Advance();
|
||||
session.Entry.Advance();
|
||||
Assert.Equal(0, session.Entry.Focus);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("14025", 14_025_000)]
|
||||
[InlineData("14.025", 14_025_000)]
|
||||
[InlineData("7025.5", 7_025_500)]
|
||||
public void AFrequencyTypedIntoTheCallBoxIsRecognised(string typed, long hertz)
|
||||
{
|
||||
RadioPosition session = Session();
|
||||
session.Entry.Call = typed;
|
||||
Assert.Equal(hertz, session.PendingQsy()?.Hertz);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("JA1XYZ")]
|
||||
[InlineData("12000")]
|
||||
public void ACallOrAnOutOfBandNumberIsNotAQsy(string typed)
|
||||
{
|
||||
RadioPosition session = Session();
|
||||
session.Entry.Call = typed;
|
||||
Assert.Null(session.PendingQsy());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AContactDeletedFromTheLogGivesItsMultiplierBack()
|
||||
{
|
||||
RadioPosition session = Session();
|
||||
Type(session, "JA1XYZ", "25");
|
||||
Qso first = session.LogContact();
|
||||
Type(session, "JA2XYZ", "25");
|
||||
session.LogContact();
|
||||
Assert.Equal(2, session.Log.Tally.TotalMultipliers);
|
||||
|
||||
session.Session.Delete(first.Id);
|
||||
Assert.Single(session.Log.Qsos);
|
||||
Assert.Equal(2, session.Log.Tally.TotalMultipliers);
|
||||
Assert.True(session.Log.Qsos.Single().IsMultiplier1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AContactWorkedSplitRecordsWhereWeTransmitted()
|
||||
{
|
||||
RadioPosition session = Session();
|
||||
session.Tune(
|
||||
Frequency.FromKilohertz(14_025),
|
||||
Modes.Cw,
|
||||
Frequency.FromKilohertz(14_200));
|
||||
Type(session, "JA1XYZ", "25");
|
||||
|
||||
Qso logged = session.LogContact();
|
||||
|
||||
Assert.Equal(14_025_000, logged.Frequency.Hertz);
|
||||
Assert.Equal(14_200_000, logged.QsxFrequency.Hertz);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AContactRecordsWhichRadioMadeIt()
|
||||
{
|
||||
RadioPosition session = new(Shared(new FakeLogStore()), 2);
|
||||
Type(session, "JA1XYZ", "25");
|
||||
|
||||
Assert.Equal(2, session.LogContact().RadioNumber);
|
||||
}
|
||||
|
||||
/// Tuning without saying anything about split leaves it where it was, so a
|
||||
/// frequency typed by hand does not clear what the radio reported.
|
||||
[Fact]
|
||||
public void TuningWithoutASplitLeavesTheOneAlreadySet()
|
||||
{
|
||||
RadioPosition session = Session();
|
||||
session.Tune(Frequency.FromKilohertz(14_025), null, Frequency.FromKilohertz(14_200));
|
||||
|
||||
session.Tune(Frequency.FromKilohertz(14_030));
|
||||
|
||||
Assert.Equal(14_200_000, session.TransmitFrequency.Hertz);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EditingAContactRescoresTheLog()
|
||||
{
|
||||
RadioPosition session = Session();
|
||||
Type(session, "JA1XYZ", "25");
|
||||
Qso logged = session.LogContact();
|
||||
Assert.Equal(3, session.Log.Tally.Points);
|
||||
|
||||
QsoEdit edit = session.Session.Edit(logged.Id, QsoField.Call, "DL2XYZ");
|
||||
|
||||
Assert.True(edit.IsAccepted);
|
||||
Assert.Equal("DL2XYZ", session.Log.Qsos.Single().Call.Text);
|
||||
Assert.Equal(0, session.Log.Tally.Points);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ARefusedEditLeavesTheLogAlone()
|
||||
{
|
||||
RadioPosition session = Session();
|
||||
Type(session, "JA1XYZ", "25");
|
||||
Qso logged = session.LogContact();
|
||||
|
||||
QsoEdit edit = session.Session.Edit(logged.Id, QsoField.Zone, "41");
|
||||
|
||||
Assert.False(edit.IsAccepted);
|
||||
Assert.Equal(25, session.Log.Qsos.Single().Zone);
|
||||
}
|
||||
|
||||
/// The other stations need the call and time the contact had before, because
|
||||
/// that is how they find their own copy of it.
|
||||
[Fact]
|
||||
public void AnEditReportsWhatTheContactUsedToBe()
|
||||
{
|
||||
RadioPosition session = Session();
|
||||
Type(session, "JA1XYZ", "25");
|
||||
Qso logged = session.LogContact();
|
||||
QsoChange? change = null;
|
||||
session.Session.Edited += (_, c) => change = c;
|
||||
|
||||
session.Session.Edit(logged.Id, QsoField.Call, "JA2XYZ");
|
||||
|
||||
Assert.Equal("JA1XYZ", change?.OldCall);
|
||||
Assert.Equal(logged.TimestampUtc, change?.OldTimestampUtc);
|
||||
Assert.Equal("JA2XYZ", change?.Qso.Call.Text);
|
||||
}
|
||||
|
||||
/// The country file gets a call wrong now and then. Correcting the country
|
||||
/// by hand has to change the score, or the correction is decoration.
|
||||
[Fact]
|
||||
public void CorrectingTheCountryChangesTheScore()
|
||||
{
|
||||
RadioPosition session = Session();
|
||||
Type(session, "JA1XYZ", "25");
|
||||
Qso logged = session.LogContact();
|
||||
Assert.Equal(3, session.Log.Tally.Points);
|
||||
|
||||
session.Session.Edit(logged.Id, QsoField.CountryPrefix, "DL");
|
||||
|
||||
Assert.Equal(0, session.Log.Tally.Points);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ADeleteReportsTheContactThatWent()
|
||||
{
|
||||
RadioPosition session = Session();
|
||||
Type(session, "JA1XYZ", "25");
|
||||
Qso logged = session.LogContact();
|
||||
Qso? gone = null;
|
||||
session.Session.Deleted += (_, q) => gone = q;
|
||||
|
||||
session.Session.Delete(logged.Id);
|
||||
|
||||
Assert.Equal("JA1XYZ", gone?.Call.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StoredContactsAreReadBackWhenTheSessionStarts()
|
||||
{
|
||||
FakeLogStore store = new();
|
||||
RadioPosition first = new(Shared(store));
|
||||
Type(first, "JA1XYZ", "25");
|
||||
first.LogContact();
|
||||
|
||||
RadioPosition second = new(new ContestSession(
|
||||
store,
|
||||
new CqWorldWide(ModeCategory.Cw),
|
||||
first.Instance,
|
||||
Me,
|
||||
CountryFile.Parse(Countries)));
|
||||
Assert.Single(second.Log.Qsos);
|
||||
Assert.Equal(3, second.Log.Tally.Points);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user