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:
@@ -20,4 +20,12 @@ public interface ContactLog
|
||||
|
||||
/// The contacts one computer made inside one block of time, oldest first.
|
||||
IReadOnlyList<Qso> Contacts(string computerName, DateTime fromUtc, TimeSpan length);
|
||||
|
||||
/// When one computer last worked anybody, as far as this log knows, or the
|
||||
/// default time when it holds nothing of that computer's.
|
||||
DateTime LastContactUtc(string computerName);
|
||||
|
||||
/// When this log starts, which is as far back as a resync can reach. The
|
||||
/// default time means the log is empty.
|
||||
DateTime FirstContactUtc { get; }
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public sealed class NetworkedStation
|
||||
|
||||
public string ContestName { get; internal set; } = "";
|
||||
|
||||
/// Where the station is, from the last `BANDMAP` it sent.
|
||||
/// Where the station is, from the last `FREQ` or `STATUS` it sent.
|
||||
public Frequency Frequency { get; internal set; }
|
||||
|
||||
public Mode? Mode { get; internal set; }
|
||||
@@ -64,17 +64,20 @@ public sealed class NetworkedStation
|
||||
|
||||
public bool IsTransmitting { get; internal set; }
|
||||
|
||||
/// What the station is sending, which is the caption of the function key
|
||||
/// it pressed. N1MM shows it in the `Msg` column while the station is
|
||||
/// transmitting and clears it when the station stops.
|
||||
public string Message { get; internal set; } = "";
|
||||
|
||||
/// True when the station is the second run radio of a two-radio position.
|
||||
/// N1MM shows Run1 or Run2/Mult in its `R/M` column.
|
||||
public bool IsRun2 { get; internal set; }
|
||||
|
||||
public int RadioNumber { get; internal set; } = 1;
|
||||
|
||||
/// A frequency this station has passed, from `PASSFREQ`, and who is on it.
|
||||
/// A frequency this station has passed, from `PASSFREQ`. N1MM sends the
|
||||
/// frequency alone: which station is on it is what the operator is about to
|
||||
/// say over the talk line.
|
||||
public Frequency PassFrequency { get; internal set; }
|
||||
|
||||
public string PassCall { get; internal set; } = "";
|
||||
|
||||
/// When anything last arrived from the station, and what it was. The window
|
||||
/// shows both: a station whose last message is minutes old is not there any
|
||||
/// more, whatever its connection says.
|
||||
@@ -111,6 +114,10 @@ public sealed class NetworkedStation
|
||||
|
||||
public string ResyncStatus { get; internal set; } = "";
|
||||
|
||||
/// Set when this station said it is ahead of us before there was a
|
||||
/// connection to answer over. The resync starts as soon as one opens.
|
||||
internal DateTime? ResyncWanted { get; set; }
|
||||
|
||||
public int HoursOk { get; internal set; }
|
||||
|
||||
public int HoursFixed { get; internal set; }
|
||||
|
||||
@@ -243,34 +243,29 @@ public sealed class StationLink : IDisposable
|
||||
public Task<int> SendTalkAsync(string text, CancellationToken cancellation = default) =>
|
||||
SendAsync(StationMessages.Talk(text, StationNumber, computerName), cancellation);
|
||||
|
||||
/// Says where this station is. It goes out whenever the radio moves or the
|
||||
/// operator turns run on or off, and it is what the other stations show and
|
||||
/// what the band-change rule counts.
|
||||
public Task<int> SendBandAsync(
|
||||
Frequency frequency,
|
||||
Mode mode,
|
||||
bool running,
|
||||
int radioNumber,
|
||||
CancellationToken cancellation = default)
|
||||
/// Says where this station is tuned. It goes out whenever the radio moves;
|
||||
/// the mode and whether the station is running go with the next `STATUS`.
|
||||
public Task<int> SendFrequencyAsync(Frequency frequency, CancellationToken cancellation = default)
|
||||
{
|
||||
mine.Frequency = frequency;
|
||||
mine.Mode = mode;
|
||||
mine.IsRunning = running;
|
||||
mine.RadioNumber = radioNumber;
|
||||
StationsChanged?.Invoke(this, EventArgs.Empty);
|
||||
return SendAsync(
|
||||
StationMessages.OnBand(frequency, mode, running, radioNumber, StationNumber, computerName),
|
||||
cancellation);
|
||||
return SendAsync(StationMessages.OnFrequency(frequency, StationNumber, computerName), cancellation);
|
||||
}
|
||||
|
||||
/// This station started or stopped transmitting, and what it is sending.
|
||||
/// `message` is the caption of the function key, which is what the other
|
||||
/// operators read in the window.
|
||||
public Task<int> SendTransmittingAsync(
|
||||
bool transmitting,
|
||||
int radioNumber,
|
||||
string message = "",
|
||||
bool isRun2 = false,
|
||||
CancellationToken cancellation = default)
|
||||
{
|
||||
mine.IsTransmitting = transmitting;
|
||||
mine.Message = transmitting ? message : "";
|
||||
StationsChanged?.Invoke(this, EventArgs.Empty);
|
||||
return SendAsync(
|
||||
StationMessages.Transmitting(transmitting, radioNumber, StationNumber, computerName),
|
||||
StationMessages.Transmitting(transmitting, message, isRun2, StationNumber, computerName),
|
||||
cancellation);
|
||||
}
|
||||
|
||||
@@ -291,20 +286,18 @@ public sealed class StationLink : IDisposable
|
||||
return SendAsync(StationMessages.Status(status, version, StationNumber, computerName), cancellation);
|
||||
}
|
||||
|
||||
/// A frequency this station is handing to the others, and who is on it.
|
||||
/// With `to` set it goes to that station alone, which is how an operator
|
||||
/// passes one contact to one position; without it every station is told,
|
||||
/// which is how the pass frequency is announced.
|
||||
/// A frequency this station is handing to the others. With `to` set it goes
|
||||
/// to that station alone, which is how an operator passes one contact to one
|
||||
/// position; without it every station is told, which is how the pass
|
||||
/// frequency is announced.
|
||||
public Task<int> SendPassAsync(
|
||||
Frequency frequency,
|
||||
string call,
|
||||
NetworkedStation? to = null,
|
||||
CancellationToken cancellation = default)
|
||||
{
|
||||
mine.PassFrequency = frequency;
|
||||
mine.PassCall = call;
|
||||
StationsChanged?.Invoke(this, EventArgs.Empty);
|
||||
StationRecord record = StationMessages.PassFrequency(frequency, call, StationNumber, computerName);
|
||||
StationRecord record = StationMessages.PassFrequency(frequency, StationNumber, computerName);
|
||||
return to is null
|
||||
? SendAsync(record, cancellation)
|
||||
: Sent(SendToAsync(to, record, cancellation));
|
||||
@@ -520,6 +513,64 @@ public sealed class StationLink : IDisposable
|
||||
// the others were told when their own connection opened
|
||||
await SendToAsync(station, StationMessages.IAm(StationNumber, computerName), cancellation)
|
||||
.ConfigureAwait(false);
|
||||
// when this station last worked anybody. The other end holds contacts
|
||||
// of ours only up to some time of its own, and starts a resync when
|
||||
// this one is later
|
||||
if (Log is not null)
|
||||
{
|
||||
await SendToAsync(
|
||||
station,
|
||||
StationMessages.LastContactAt(Log.LastContactUtc(computerName), StationNumber, computerName),
|
||||
cancellation).ConfigureAwait(false);
|
||||
}
|
||||
// that station told us it was ahead of us before there was anything to
|
||||
// answer over
|
||||
if (station.ResyncWanted is { } behindSince)
|
||||
{
|
||||
station.ResyncWanted = null;
|
||||
StartResync(station, behindSince, DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
|
||||
/// A station said when it last worked anybody. When this station holds
|
||||
/// nothing of its that late, the hours in between are missing and a resync
|
||||
/// asks for them.
|
||||
///
|
||||
/// An empty log asks for nothing: a station that has just been started has
|
||||
/// no business pulling the whole contest across, and N1MM suppresses it the
|
||||
/// same way.
|
||||
private void ResyncIfBehind(NetworkedStation station, DateTime theirLast)
|
||||
{
|
||||
if (Log is null || station.IsResyncing || theirLast == default)
|
||||
{
|
||||
return;
|
||||
}
|
||||
DateTime ourLast = Log.LastContactUtc(station.ComputerName);
|
||||
if (theirLast <= ourLast)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// nothing at all from that station: everything it worked since this log
|
||||
// started is missing
|
||||
DateTime from = ourLast == default ? Log.FirstContactUtc : ourLast;
|
||||
if (from == default)
|
||||
{
|
||||
Failed?.Invoke(
|
||||
this,
|
||||
$"{station.ComputerName} has worked somebody since this log was started, and this log is empty — resync by hand");
|
||||
return;
|
||||
}
|
||||
Failed?.Invoke(this, $"resynchronising with {station.ComputerName}, which has contacts this log has not");
|
||||
// the message arrived over the connection that station opened, and the
|
||||
// answer goes over the one this station opens, which may not be up yet
|
||||
if (writers.ContainsKey(station.ComputerName))
|
||||
{
|
||||
StartResync(station, from, DateTime.UtcNow);
|
||||
}
|
||||
else
|
||||
{
|
||||
station.ResyncWanted = from;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AcceptAsync(CancellationToken cancellation)
|
||||
@@ -592,11 +643,11 @@ public sealed class StationLink : IDisposable
|
||||
case "IAM":
|
||||
station.StationNumber = record.Number(0);
|
||||
break;
|
||||
case "BANDMAP":
|
||||
station.Frequency = Frequency.FromKilohertz(record.Decimal(0));
|
||||
station.Mode = Modes.Parse(record.Field(1));
|
||||
station.IsRunning = record.Flag(2);
|
||||
station.RadioNumber = Math.Max(1, record.Number(3));
|
||||
case "FREQ":
|
||||
station.Frequency = Frequency.FromKilohertz(record.Decimal(0) / 100);
|
||||
break;
|
||||
case "LASTQAT":
|
||||
ResyncIfBehind(station, record.Time(0));
|
||||
break;
|
||||
case "STATUS":
|
||||
StationStatus status = StationMessages.ReadStatus(record);
|
||||
@@ -611,11 +662,11 @@ public sealed class StationLink : IDisposable
|
||||
break;
|
||||
case "XMIT":
|
||||
station.IsTransmitting = record.Flag(0);
|
||||
station.RadioNumber = Math.Max(1, record.Number(1));
|
||||
station.Message = station.IsTransmitting ? record.Field(1) : "";
|
||||
station.IsRun2 = record.Flag(2);
|
||||
break;
|
||||
case "PASSFREQ":
|
||||
station.PassFrequency = Frequency.FromKilohertz(record.Decimal(0));
|
||||
station.PassCall = record.Field(1);
|
||||
station.PassFrequency = Frequency.FromKilohertz(record.Decimal(0) / 100);
|
||||
break;
|
||||
case "ECHOREQ":
|
||||
_ = SendAsync(StationMessages.Echo(record, StationNumber, computerName));
|
||||
@@ -664,7 +715,7 @@ public sealed class StationLink : IDisposable
|
||||
return;
|
||||
}
|
||||
int checksum = LogChecksum.Of(Log.Contacts(station.ComputerName, fromUtc, ResyncBlock));
|
||||
await SendToAsync(
|
||||
bool sent = await SendToAsync(
|
||||
station,
|
||||
StationMessages.CheckSum(
|
||||
fromUtc,
|
||||
@@ -677,6 +728,14 @@ public sealed class StationLink : IDisposable
|
||||
rescoreWhenDone,
|
||||
StationNumber,
|
||||
computerName)).ConfigureAwait(false);
|
||||
if (!sent)
|
||||
{
|
||||
// nothing to ask over. Left running, the row would say it is
|
||||
// resynchronising for the rest of the contest
|
||||
station.IsResyncing = false;
|
||||
station.ResyncStatus = "Stopped: no connection";
|
||||
StationsChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// A station asked whether we hold the same block. This end is the
|
||||
|
||||
@@ -21,7 +21,8 @@ namespace Nonemm.Network;
|
||||
/// | `TALK` | a line of chat between operators | yes | yes |
|
||||
/// | `PASSFREQ` | a frequency being passed to another radio | yes | yes |
|
||||
/// | `XMIT` | a station started or stopped transmitting | yes | yes |
|
||||
/// | `BANDMAP` | which band and mode a station is on | yes | yes |
|
||||
/// | `FREQ` | where a station is tuned | yes | yes |
|
||||
/// | `LASTQAT` | the time of the sender's last contact, which starts a resync | yes | yes |
|
||||
/// | `STATUS` | everything the network status window shows about a station | yes | yes |
|
||||
/// | `CHECKSUM` | the checksum of one block of the log, which starts a resync | yes | yes |
|
||||
/// | `CONFIRMED` | the answer to a checksum, and what moves a resync on | yes | yes |
|
||||
@@ -93,52 +94,57 @@ public static class StationMessages
|
||||
public static StationRecord Talk(string text, int stationNumber, string computerName) =>
|
||||
Record("TALK", stationNumber, computerName, [$"[{computerName}] {text}"]);
|
||||
|
||||
/// A frequency handed to another radio, which is N1MM's pass. The
|
||||
/// callsign is who is on it.
|
||||
/// A frequency handed to another radio, which is N1MM's pass. It carries
|
||||
/// the frequency and nothing else — no callsign — in hundredths of a
|
||||
/// kilohertz.
|
||||
public static StationRecord PassFrequency(
|
||||
Frequency frequency,
|
||||
string call,
|
||||
int stationNumber,
|
||||
string computerName) =>
|
||||
Record(
|
||||
"PASSFREQ",
|
||||
stationNumber,
|
||||
computerName,
|
||||
[StationRecord.Written(frequency.Kilohertz), call]);
|
||||
Record("PASSFREQ", stationNumber, computerName, [Hundredths(frequency)]);
|
||||
|
||||
/// A station started or stopped transmitting. The other stations of a
|
||||
/// multi-single entry need this: two of them keying at once is one signal
|
||||
/// too many.
|
||||
/// Where this station is tuned. It goes out whenever the radio moves, and
|
||||
/// the mode and whether the station is running arrive in `STATUS`.
|
||||
public static StationRecord OnFrequency(
|
||||
Frequency frequency,
|
||||
int stationNumber,
|
||||
string computerName) =>
|
||||
Record("FREQ", stationNumber, computerName, [Hundredths(frequency)]);
|
||||
|
||||
/// The time of the last contact this station made. It goes out as soon as a
|
||||
/// connection opens: a station that has been away holds nothing that late
|
||||
/// and starts a resync on the strength of it.
|
||||
public static StationRecord LastContactAt(
|
||||
DateTime lastUtc,
|
||||
int stationNumber,
|
||||
string computerName) =>
|
||||
Record("LASTQAT", stationNumber, computerName, [StationRecord.Written(lastUtc)]);
|
||||
|
||||
/// A station started or stopped transmitting, and what it is sending: N1MM
|
||||
/// carries the caption of the function key, which is what the other
|
||||
/// operators read in the `Msg` column. Two stations of a multi-single entry
|
||||
/// keying at once is one signal too many.
|
||||
///
|
||||
/// The last two fields are whether the F1 key was the one pressed and
|
||||
/// whether the other stations are being told to stop sending. Both go out
|
||||
/// as false: this program neither reports which key was pressed nor stops
|
||||
/// another station's transmitter.
|
||||
public static StationRecord Transmitting(
|
||||
bool transmitting,
|
||||
int radioNumber,
|
||||
string message,
|
||||
bool isRun2,
|
||||
int stationNumber,
|
||||
string computerName) =>
|
||||
Record(
|
||||
"XMIT",
|
||||
stationNumber,
|
||||
computerName,
|
||||
[StationRecord.Written(transmitting), Text(radioNumber)]);
|
||||
|
||||
/// Where a station is: the band and the mode it is on, and whether it is
|
||||
/// running. This is what fills the band and Running columns of the network
|
||||
/// status window, and what the band-change rule counts.
|
||||
public static StationRecord OnBand(
|
||||
Frequency frequency,
|
||||
Mode mode,
|
||||
bool running,
|
||||
int radioNumber,
|
||||
int stationNumber,
|
||||
string computerName) =>
|
||||
Record(
|
||||
"BANDMAP",
|
||||
stationNumber,
|
||||
computerName,
|
||||
[
|
||||
StationRecord.Written(frequency.Kilohertz),
|
||||
mode.Name,
|
||||
StationRecord.Written(running),
|
||||
Text(radioNumber),
|
||||
StationRecord.Written(transmitting),
|
||||
message,
|
||||
StationRecord.Written(isRun2),
|
||||
StationRecord.Written(false),
|
||||
StationRecord.Written(false),
|
||||
]);
|
||||
|
||||
/// Everything a station says about itself, sent every few seconds. The two
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Nonemm.Network;
|
||||
/// rows of the network status window.
|
||||
///
|
||||
/// It is one message rather than a field on each of the others because N1MM
|
||||
/// sends it that way: `BANDMAP` says a station moved, `STATUS` says everything
|
||||
/// sends it that way: `FREQ` says a station moved, `STATUS` says everything
|
||||
/// the window shows about it and arrives whether anything moved or not, so a
|
||||
/// station that has been sitting on one frequency for an hour still has a
|
||||
/// current row.
|
||||
|
||||
Reference in New Issue
Block a user