Send contacts in the layout N1MM actually reads

QsoRecord followed MultiOpManager.QSOString, a helper N1MM does not use
for the network. The messages use Contact.QSOString: 43 fields, the
continent before the contact type, and flags as numbers. N1MM converts
the multiplier flags with ToShort and threw on 'True', so every contact
logged here raised an error at the other computer.

The id now goes out as a GUID and is kept on arrival, which is what the
other station's checksum of a resync block is made over.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 00:41:51 +00:00
co-authored by Claude Sonnet 5.5
parent 0a423e9e1f
commit f336b89c37
2 changed files with 128 additions and 31 deletions
+52 -31
View File
@@ -3,32 +3,38 @@ using Nonemm.Core;
namespace Nonemm.Network;
/// A contact as it travels between two logging computers: the thirty-five
/// fields of N1MM's `QSOString`, in N1MM's order.
/// A contact as it travels between two logging computers: the forty-three
/// fields of N1MM's `Contact.QSOString`, in N1MM's order.
///
/// The order is what matters and it cannot be changed, because the other end
/// reads the fields by position. It is taken from `MultiOpManager.cs:1007`.
/// reads the fields by position, with `SetNewContact`. N1MM converts a flag
/// with `ToShort`, so it has to be a number: it throws on `True`. The only
/// flag it reads in words as well is `IsOriginal`.
///
/// | At | Field | At | Field |
/// |---|---|---|---|
/// | 0 | timestamp | 18 | sent serial |
/// | 1 | callsign | 19 | points |
/// | 2 | frequency, kHz | 20 | multiplier 1 |
/// | 3 | transmit frequency | 21 | multiplier 2 |
/// | 4 | mode | 22 | power |
/// | 5 | contest name | 23 | band, MHz |
/// | 6 | sent report | 24 | WPX prefix |
/// | 7 | received report | 25 | exchange 1 |
/// | 8 | country prefix | 26 | radio number |
/// | 9 | station prefix | 27 | operator |
/// | 10 | QTH | 28 | grid square |
/// | 11 | name | 29 | contest number |
/// | 12 | comment | 30 | multiplier 3 |
/// | 13 | received serial | 31 | misc text |
/// | 14 | section | 32 | contact type |
/// | 15 | precedence | 33 | run 1 or run 2 |
/// | 16 | check | 34 | continent |
/// | 17 | zone | | |
/// | 0 | timestamp | 22 | power |
/// | 1 | callsign | 23 | band, MHz |
/// | 2 | frequency, kHz | 24 | WPX prefix |
/// | 3 | transmit frequency | 25 | exchange 1 |
/// | 4 | mode | 26 | radio number |
/// | 5 | contest name | 27 | operator |
/// | 6 | sent report | 28 | grid square |
/// | 7 | received report | 29 | contest number |
/// | 8 | country prefix | 30 | multiplier 3 |
/// | 9 | station prefix | 31 | misc text |
/// | 10 | QTH | 32 | continent |
/// | 11 | name | 33 | contact type |
/// | 12 | comment | 34 | run 1 or run 2 |
/// | 13 | received serial | 35 | rover location |
/// | 14 | section | 36 | radio interfaced |
/// | 15 | precedence | 37 | station number |
/// | 16 | check | 38 | computer name |
/// | 17 | zone | 39 | original |
/// | 18 | sent serial | 40 | run QSO |
/// | 19 | points | 41 | id |
/// | 20 | multiplier 1 | 42 | claimed |
/// | 21 | multiplier 2 | | |
///
/// The contact carries its points and its multiplier flags, and they are read
/// rather than trusted: the log works them out again from the contest rules, so
@@ -38,7 +44,7 @@ public static class QsoRecord
{
/// How many fields there are. A message with fewer is read as far as it
/// goes, because a station on another version sends a shorter one.
public const int FieldCount = 35;
public const int FieldCount = 43;
public static List<string> Write(Qso qso) =>
[
@@ -63,8 +69,8 @@ public static class QsoRecord
Text(qso.Zone),
Text(qso.SentNumber),
Text(qso.Points),
StationRecord.Written(qso.IsMultiplier1),
StationRecord.Written(qso.IsMultiplier2),
Flag(qso.IsMultiplier1),
Flag(qso.IsMultiplier2),
qso.Power,
StationRecord.Written(qso.Band?.MegahertzLabel ?? 0),
qso.WpxPrefix,
@@ -73,11 +79,20 @@ public static class QsoRecord
qso.Operator,
qso.GridSquare,
Text(qso.ContestNumber),
StationRecord.Written(qso.IsMultiplier3),
Flag(qso.IsMultiplier3),
qso.MiscText,
qso.Continent,
qso.ContactType,
Text(qso.RunPosition),
qso.Continent,
qso.RoverLocation,
Flag(qso.IsRadioInterfaced),
Text(qso.NetworkedComputerNumber),
qso.StationName,
Flag(qso.IsOriginal),
Flag(qso.IsRunQso),
// N1MM parses this as a GUID and throws on anything else
Guid.TryParse(qso.Id, out Guid id) ? id.ToString("N") : "",
Flag(qso.IsClaimed),
];
/// The contact the fields describe. `at` is where the fields start, which
@@ -96,7 +111,7 @@ public static class QsoRecord
Frequency transmit = Frequency.FromKilohertz(record.Decimal(at + 3));
return new Qso
{
Id = Qso.NewId(),
Id = Guid.TryParse(record.Field(at + 41), out Guid sent) ? sent.ToString("N") : Qso.NewId(),
TimestampUtc = record.Time(at),
Call = Callsign.Parse(record.Field(at + 1)),
Frequency = frequency,
@@ -128,15 +143,21 @@ public static class QsoRecord
ContestNumber = record.Number(at + 29),
IsMultiplier3 = record.Flag(at + 30),
MiscText = record.Field(at + 31),
ContactType = record.Field(at + 32),
RunPosition = record.Number(at + 33),
Continent = record.Field(at + 34),
Continent = record.Field(at + 32),
ContactType = record.Field(at + 33),
RunPosition = record.Number(at + 34),
RoverLocation = record.Field(at + 35),
IsRunQso = record.Flag(at + 40),
// a station on an older version stops before this
IsClaimed = record.Field(at + 42).Length == 0 || record.Flag(at + 42),
StationName = stationName,
NetworkedComputerNumber = record.StationNumber,
NetworkedComputerNumber = record.Number(at + 37) is > 0 and var made ? made : record.StationNumber,
// it was made at another radio, on another computer
IsOriginal = false,
};
}
private static string Flag(bool flag) => flag ? "1" : "0";
private static string Text(int number) => number.ToString(CultureInfo.InvariantCulture);
}