Send the messages N1MM actually reads

Three of the messages this program sends were its own invention or in the
wrong units, so an N1MM beside it read them wrongly or not at all.

| Message | N1MM | What was sent |
|---|---|---|
| `XMIT` | transmitting, the function key's caption, Run2, F1 pressed, stop sending | transmitting, the radio number |
| `PASSFREQ` | the frequency in hundredths of a kilohertz, and nothing else | the frequency in kilohertz, and a callsign |
| `FREQ` | the frequency in hundredths of a kilohertz | `BANDMAP`, which N1MM has no such message for |

So N1MM showed a radio number where the other operators read what a
station is sending, read a pass on 14205.5 as 140.25, and never saw this
station move at all. `BANDMAP` is gone: the frequency goes out in `FREQ`
when the radio moves and the mode, run and the rest ride in the `STATUS`
that follows it, which is where N1MM carries them.

The `Msg` column now shows what a station is sending, which is what the
caption is for. `PassCall` and `RadioNumber` are gone from the station:
neither is on the wire, so neither was ever anything but this program
talking to itself.

**A station that reconnects resynchronises by itself.** N1MM sends
`LASTQAT` — when I last worked anybody — as soon as a connection opens,
and a station holding nothing that late asks for the hours in between. An
empty log asks for nothing: a program that has just been started has no
business pulling the whole contest across, and N1MM suppresses it the same
way. The answer goes over the connection this station opens, which is not
always up when the message arrives, so a resync that has nothing to ask
over waits for it rather than being lost — and one that still cannot send
stops and says so instead of leaving the row resynchronising for the rest
of the contest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
This commit is contained in:
2026-09-03 22:54:16 +00:00
co-authored by Claude Opus 5
parent 2a21c772ad
commit b5d86c1677
11 changed files with 297 additions and 90 deletions
+96 -4
View File
@@ -57,8 +57,10 @@ public class StationLinkTests
/// The band a station is on is what the status window shows and what the
/// band-change rule counts.
/// N1MM carries the frequency in hundredths of a kilohertz, in its own
/// `FREQ` message. A tenth of a hertz survives the trip.
[Fact]
public async Task WhereAStationIsReachesTheOthers()
public async Task WhereAStationIsTunedReachesTheOthers()
{
(int onePort, int twoPort) = SparePorts();
using StationLink one = new("RUN-PC", Version, stationNumber: 1, port: onePort);
@@ -68,14 +70,95 @@ public class StationLinkTests
one.AddStation("MULT-PC", "127.0.0.1", twoPort);
await WaitForAsync(() => one.Stations.Any(s => s is { IsMine: false, IsConnected: true }));
await one.SendBandAsync(Frequency.FromKilohertz(21_025), Modes.Cw, running: true, radioNumber: 1);
await one.SendFrequencyAsync(Frequency.FromKilohertz(21_025.25));
await WaitForAsync(() =>
two.Stations.FirstOrDefault(s => s.ComputerName == "RUN-PC")?.Band?.Name == "15M");
NetworkedStation? seen = two.Stations.FirstOrDefault(s => s.ComputerName == "RUN-PC");
Assert.Equal("15M", seen?.Band?.Name);
Assert.Equal("CW", seen?.Mode?.Name);
Assert.True(seen?.IsRunning);
Assert.Equal(21_025.25, seen?.Frequency.Kilohertz ?? 0, 2);
}
/// What a station is sending is what the other operators read in the `Msg`
/// column, and N1MM carries it in the transmit message.
[Fact]
public async Task WhatAStationIsSendingReachesTheOthers()
{
(int onePort, int twoPort) = SparePorts();
using StationLink one = new("RUN-PC", Version, stationNumber: 1, port: onePort);
using StationLink two = new("MULT-PC", Version, stationNumber: 2, port: twoPort);
one.Start();
two.Start();
one.AddStation("MULT-PC", "127.0.0.1", twoPort);
await WaitForAsync(() => one.Stations.Any(s => s is { IsMine: false, IsConnected: true }));
await one.SendTransmittingAsync(true, "F1 CQ", isRun2: true);
await WaitForAsync(() =>
two.Stations.FirstOrDefault(s => s.ComputerName == "RUN-PC")?.IsTransmitting == true);
NetworkedStation? seen = two.Stations.FirstOrDefault(s => s.ComputerName == "RUN-PC");
Assert.Equal("F1 CQ", seen?.Message);
Assert.True(seen?.IsRun2);
await one.SendTransmittingAsync(false);
await WaitForAsync(() =>
two.Stations.FirstOrDefault(s => s.ComputerName == "RUN-PC")?.IsTransmitting == false);
Assert.Equal("", two.Stations.First(s => s.ComputerName == "RUN-PC").Message);
}
/// A pass frequency is the frequency and nothing else, in hundredths of a
/// kilohertz.
[Fact]
public async Task APassFrequencyReachesTheOthers()
{
(int onePort, int twoPort) = SparePorts();
using StationLink one = new("RUN-PC", Version, stationNumber: 1, port: onePort);
using StationLink two = new("MULT-PC", Version, stationNumber: 2, port: twoPort);
one.Start();
two.Start();
one.AddStation("MULT-PC", "127.0.0.1", twoPort);
await WaitForAsync(() => one.Stations.Any(s => s is { IsMine: false, IsConnected: true }));
await one.SendPassAsync(Frequency.FromKilohertz(14_205.5));
await WaitForAsync(() =>
two.Stations.FirstOrDefault(s => s.ComputerName == "RUN-PC")?.PassFrequency.Hertz > 0);
Assert.Equal(
14_205.5,
two.Stations.First(s => s.ComputerName == "RUN-PC").PassFrequency.Kilohertz,
1);
}
/// A station that reconnects is told when the other one last worked
/// somebody, and asks for what it is missing without anybody pressing a
/// button.
[Fact]
public async Task AStationThatIsBehindResynchronisesOnItsOwn()
{
(int onePort, int twoPort) = SparePorts();
using StationLink one = new("RUN-PC", Version, stationNumber: 1, port: onePort);
using StationLink two = new("MULT-PC", Version, stationNumber: 2, port: twoPort);
DateTime hour = DateTime.UtcNow.AddMinutes(-30);
Qso first = Contact() with { TimestampUtc = hour, Id = "first" };
Qso later = Contact() with
{
TimestampUtc = hour.AddMinutes(10),
Call = Callsign.Parse("DL1ABC"),
Id = "later",
};
one.Log = new FakeLog([first, later]);
// MULT-PC has the first contact and missed the one after it
FakeLog multLog = new([first]);
two.Log = multLog;
two.UpdateArrived += (_, update) => multLog.Take(update);
one.Start();
two.Start();
one.AddStation("MULT-PC", "127.0.0.1", twoPort);
two.AddStation("RUN-PC", "127.0.0.1", onePort);
await WaitForAsync(() => multLog.All.Count == 2);
Assert.Equal(["DL1ABC", "G3XYZ"], multLog.All.Select(q => q.Call.Text).Order());
}
/// The status message carries what the network status window shows: the
@@ -291,6 +374,15 @@ public class StationLinkTests
public IReadOnlyList<Qso> Contacts(string computerName, DateTime fromUtc, TimeSpan length) =>
LogChecksum.Block(qsos, computerName, fromUtc, length);
public DateTime LastContactUtc(string computerName) =>
qsos.Where(q => string.Equals(q.StationName, computerName, StringComparison.OrdinalIgnoreCase))
.Select(q => q.TimestampUtc)
.DefaultIfEmpty(default)
.Max();
public DateTime FirstContactUtc =>
qsos.Select(q => q.TimestampUtc).DefaultIfEmpty(default).Min();
public void Take(ContactUpdate update)
{
switch (update)