Show the other computers, and apply the ten-minute rule
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
This commit is contained in:
@@ -25,6 +25,12 @@ public sealed class AppSession : IDisposable
|
||||
private int activeRadio;
|
||||
private ClusterClient? cluster;
|
||||
private StationNetwork? network;
|
||||
private StationLink? link;
|
||||
|
||||
/// What was last said to the other stations about where this one is, so it
|
||||
/// is said again only when it changes.
|
||||
private (Frequency Where, string Mode, bool Running, int Radio) announced;
|
||||
private DispatcherTimer? announcing;
|
||||
private MessageSender? keyer;
|
||||
private AlternatingCq? alternating;
|
||||
private MmttyEngine? digital;
|
||||
@@ -124,6 +130,10 @@ public sealed class AppSession : IDisposable
|
||||
|
||||
public StationNetwork? Network => network;
|
||||
|
||||
/// The link to the other logging computers, or null when the operator has
|
||||
/// not turned it on. The network status window reads it.
|
||||
public StationLink? Link => link;
|
||||
|
||||
public MessageSender? Keyer => keyer;
|
||||
|
||||
/// The digital modem, started by the digital window rather than at startup:
|
||||
@@ -221,6 +231,12 @@ public sealed class AppSession : IDisposable
|
||||
Logging.Edited += (_, change) => _ = network?.SendEditAsync(
|
||||
change.Qso, Settings.Station.Callsign, change.OldCall, change.OldTimestampUtc);
|
||||
Logging.Deleted += (_, qso) => _ = network?.SendDeleteAsync(qso, Settings.Station.Callsign);
|
||||
// and the same three to the other logging computers, which is a
|
||||
// different protocol on a different port
|
||||
Logging.Logged += (_, qso) => _ = link?.SendLoggedAsync(qso);
|
||||
Logging.Edited += (_, change) =>
|
||||
_ = link?.SendEditedAsync(change.Qso, change.OldCall, change.OldTimestampUtc);
|
||||
Logging.Deleted += (_, qso) => _ = link?.SendDeletedAsync(qso);
|
||||
Check = new CheckWindowSources(positions[0], Calls, Bandmap);
|
||||
Available = new AvailableStations(positions[0], Bandmap);
|
||||
Save(Settings with { ContestNumber = contestNumber });
|
||||
@@ -234,7 +250,9 @@ public sealed class AppSession : IDisposable
|
||||
/// multi-operator entry, following what the settings now say.
|
||||
public void ApplyNetworkSettings()
|
||||
{
|
||||
announcing?.Stop();
|
||||
network?.Dispose();
|
||||
link?.Dispose();
|
||||
network = null;
|
||||
if (!Settings.NetworkEnabled)
|
||||
{
|
||||
@@ -253,6 +271,87 @@ public sealed class AppSession : IDisposable
|
||||
Changed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// Starts, restarts or stops the link to the other logging computers. It is
|
||||
/// separate from `ApplyNetworkSettings` because the two are separate
|
||||
/// networks: one carries XML to other programs, the other carries contacts
|
||||
/// to the other computers of this entry.
|
||||
public void ApplyStationLinkSettings()
|
||||
{
|
||||
link?.Dispose();
|
||||
link = null;
|
||||
if (!Settings.StationLinkEnabled)
|
||||
{
|
||||
Changed?.Invoke(this, EventArgs.Empty);
|
||||
return;
|
||||
}
|
||||
link = new StationLink(
|
||||
Settings.NetworkStationName.Length > 0 ? Settings.NetworkStationName : Environment.MachineName,
|
||||
Settings.StationLinkVersion,
|
||||
Settings.StationNumber,
|
||||
Settings.StationLinkPort)
|
||||
{
|
||||
Operator = Settings.Station.Callsign,
|
||||
};
|
||||
// the socket thread must not touch the log: every other change to it is
|
||||
// made where the windows read it
|
||||
link.UpdateArrived += (_, update) =>
|
||||
Dispatcher.UIThread.Post(() => TakeFromNetwork(update));
|
||||
foreach (string peer in Settings.StationLinkPeers)
|
||||
{
|
||||
if (Peer(peer) is var (name, address, port))
|
||||
{
|
||||
link.AddStation(name, address, port);
|
||||
}
|
||||
}
|
||||
link.Start();
|
||||
// where this station is has a dozen places it can change from — the
|
||||
// radio moving, a band button, a QSY typed into the callsign box, run
|
||||
// turning on — so it is read once a second and sent when it has
|
||||
// changed, rather than announced from each of them
|
||||
announcing?.Stop();
|
||||
announced = default;
|
||||
announcing = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||
announcing.Tick += (_, _) => AnnounceWhereIAm();
|
||||
announcing.Start();
|
||||
Changed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
/// Tells the other stations where this one is, if it has moved. In a
|
||||
/// multi-single entry this is what the other operators watch: two stations
|
||||
/// on one band is a contact nobody can make.
|
||||
private void AnnounceWhereIAm()
|
||||
{
|
||||
if (link is null || Position is not { } position)
|
||||
{
|
||||
return;
|
||||
}
|
||||
(Frequency, string, bool, int) now =
|
||||
(position.Frequency, position.Mode.Name, position.IsRunning, position.RadioNumber);
|
||||
if (now == announced)
|
||||
{
|
||||
return;
|
||||
}
|
||||
announced = now;
|
||||
_ = link.SendBandAsync(position.Frequency, position.Mode, position.IsRunning, position.RadioNumber);
|
||||
}
|
||||
|
||||
/// A station named by hand, written `RUN-PC@192.168.1.5` with `:port` on
|
||||
/// the end when that station is not on the usual one. Null for anything
|
||||
/// else, because a line the operator has half typed is not an address.
|
||||
private (string Name, string Address, int Port)? Peer(string text)
|
||||
{
|
||||
string[] parts = text.Split('@');
|
||||
if (parts.Length != 2 || parts[0].Trim().Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
string[] host = parts[1].Split(':');
|
||||
return (
|
||||
parts[0].Trim(),
|
||||
host[0].Trim(),
|
||||
host.Length > 1 && int.TryParse(host[1], out int given) ? given : Settings.StationLinkPort);
|
||||
}
|
||||
|
||||
/// What another station did to its log, applied to ours. Nothing goes back
|
||||
/// out to the network, and the contact is scored here from the rules
|
||||
/// instead of trusting what the sender put in the message.
|
||||
@@ -595,7 +694,9 @@ public sealed class AppSession : IDisposable
|
||||
spotFlush.Dispose();
|
||||
DisposeRadios();
|
||||
cluster?.Dispose();
|
||||
announcing?.Stop();
|
||||
network?.Dispose();
|
||||
link?.Dispose();
|
||||
alternating?.Dispose();
|
||||
keyer?.Dispose();
|
||||
box?.Dispose();
|
||||
|
||||
Reference in New Issue
Block a user