Talk to the other logging computers on N1MM's own port

N1MM has two networks and this program had only one of them. Port 12060
carries XML to other programs — a spot tool, a score poster — and
`StationNetwork` already writes that. Computer to computer, N1MM uses
port 12070 and a different protocol, which is what this adds.

The wire format, from `MultiOpManager` and `MultiStation`:

    DATA__07%SHACK-PC%QSO%2026-09-03 12:34:56%DL1ABC%…~__DATA

the sending station's number, its computer name, the message type and
the fields of that type. `%` and `~` cannot appear in a field, so N1MM
writes `!` in their place. `StationRecord` reads and writes that, and
holds what has arrived until a `~` says a message is whole: TCP hands
over half a message as often as two.

Discovery is a UDP broadcast to the same port, six fields wide.
**N1MM refuses a station whose version is not its own** — it says
"Software versions must match" and drops it — so the version this
program broadcasts is a setting rather than its own version, and a
station that says something else is kept in the list with `Refused` set
so the operator can see why nothing is arriving.

`StationLink` runs both sockets, opens a connection to every station it
hears, and reports what arrives. `QsoRecord` is the thirty-five fields
of a contact in N1MM's order, from `MultiOpManager.cs:1007`; points and
multiplier flags are read but the log works them out again from the
rules, so two stations cannot disagree about a score. `AddStation` names
a station by hand, for a network where a broadcast does not reach.

A station can also be named rather than heard, and one that is connected
may say nothing for a while, so `NetworkedStation` is heard-from as soon
as it exists. Left at nothing, the sweep that drops quiet stations read
every new station as quiet since the beginning of time and closed the
connection the moment it opened.

Tested as bytes and over loopback. Nothing has talked to a real N1MM.

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 15:19:03 +00:00
co-authored by Claude Opus 5
parent 9f635550bd
commit c0e1bc1e1b
8 changed files with 1643 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
using Nonemm.Core;
namespace Nonemm.Network;
/// One computer on the network, as far as this one can tell. The network status
/// window shows a row per station and these are its columns.
///
/// Everything here is what the station last said, with the time it said it.
/// Nothing is asked for on demand: the beacons and the messages carry it, and a
/// station that has gone quiet keeps the last thing it said with an older
/// `LastHeardUtc`, which is how the window shows it as missing rather than
/// blank.
public sealed class NetworkedStation
{
public NetworkedStation(string computerName, string address, int port)
{
ComputerName = computerName;
Address = address;
Port = port;
// now is when it was heard of, whether that was a beacon or the
// operator naming it. Left at nothing, `StationLink.Forget` reads a
// station as having been quiet since the beginning of time and drops
// the connection to it the moment it is opened
LastHeardUtc = DateTime.UtcNow;
}
/// The name in every message the station sends, and what a contact from it
/// is stamped with. N1MM uses the computer's own network name.
public string ComputerName { get; }
public string Address { get; internal set; }
public int Port { get; internal set; }
/// Which station of the entry this is. N1MM numbers them so a contact can
/// say which position made it; the number arrives in `IAM`.
public int StationNumber { get; internal set; }
/// The version the station broadcast. N1MM refuses to talk to a station
/// whose version is not its own, so a mismatch here is why nothing works.
public string Version { get; internal set; } = "";
public string Operator { get; internal set; } = "";
public string ContestName { get; internal set; } = "";
/// Where the station is, from the last `BANDMAP` it sent.
public Frequency Frequency { get; internal set; }
public Mode? Mode { get; internal set; }
public Band? Band => Bands.ForFrequency(Frequency);
/// True while the station is running rather than searching. In a
/// multi-single entry this is the run station.
public bool IsRunning { get; internal set; }
public bool IsTransmitting { get; internal set; }
public int RadioNumber { get; internal set; } = 1;
/// A frequency this station has passed, from `PASSFREQ`, and who is on it.
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.
public DateTime LastHeardUtc { get; internal set; }
public string LastMessage { get; internal set; } = "";
/// How many messages have gone each way since the program started. N1MM
/// shows the same two numbers, and a Send that climbs while Read stands
/// still is a station that is not listening.
public int Sent { get; internal set; }
public int Read { get; internal set; }
/// True while a connection to the station is open. It is not the same as
/// the station being there: the connection can stand for a while after the
/// other program has stopped.
public bool IsConnected { get; internal set; }
/// How long the last echo took to come back, or null when none has.
public TimeSpan? EchoTime { get; internal set; }
/// This computer, which is in the list as well. N1MM shows it too, so an
/// operator can read its own station number and version off the same
/// window.
public bool IsMine { get; internal init; }
/// Set when the station broadcast a version that is not ours. N1MM turns
/// such a station away, so this program says why rather than failing to
/// connect for no visible reason.
public string Refused { get; internal set; } = "";
}