Files
Nonemm/src/Nonemm.Network/QsoRecord.cs
T
erikbrockoandClaude Sonnet 5.5 f336b89c37 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>
2026-10-24 00:41:51 +00:00

164 lines
6.6 KiB
C#

using System.Globalization;
using Nonemm.Core;
namespace Nonemm.Network;
/// 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, 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 | 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
/// two stations cannot disagree about a score because one of them was running
/// an older set of rules.
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 = 43;
public static List<string> Write(Qso qso) =>
[
StationRecord.Written(qso.TimestampUtc),
qso.Call.Text,
StationRecord.Written(qso.Frequency.Kilohertz),
StationRecord.Written(
(qso.QsxFrequency.Hertz == 0 ? qso.Frequency : qso.QsxFrequency).Kilohertz),
qso.Mode.Name,
qso.ContestName,
qso.SentReport,
qso.ReceivedReport,
qso.CountryPrefix,
qso.StationPrefix,
qso.Qth,
qso.Name,
qso.Comment,
Text(qso.ReceivedNumber),
qso.Section,
qso.Precedence,
Text(qso.Check),
Text(qso.Zone),
Text(qso.SentNumber),
Text(qso.Points),
Flag(qso.IsMultiplier1),
Flag(qso.IsMultiplier2),
qso.Power,
StationRecord.Written(qso.Band?.MegahertzLabel ?? 0),
qso.WpxPrefix,
qso.Exchange1,
Text(qso.RadioNumber),
qso.Operator,
qso.GridSquare,
Text(qso.ContestNumber),
Flag(qso.IsMultiplier3),
qso.MiscText,
qso.Continent,
qso.ContactType,
Text(qso.RunPosition),
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
/// is not always 0: an edit puts the old timestamp in front of them and a
/// replace the old callsign as well.
///
/// `stationName` is the computer that sent it, kept on the contact so the
/// log window can say where each row came from.
///
/// The band, field 23, is not read back: this program works the band out
/// from the frequency, and reading it would believe a station that
/// disagreed with the band plan about where 14.2 MHz is.
public static Qso Read(StationRecord record, int at, string stationName)
{
Frequency frequency = Frequency.FromKilohertz(record.Decimal(at + 2));
Frequency transmit = Frequency.FromKilohertz(record.Decimal(at + 3));
return new Qso
{
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,
QsxFrequency = transmit == frequency ? Frequency.Zero : transmit,
Mode = Modes.Parse(record.Field(at + 4)) ?? Modes.Cw,
ContestName = record.Field(at + 5),
SentReport = record.Field(at + 6),
ReceivedReport = record.Field(at + 7),
CountryPrefix = record.Field(at + 8),
StationPrefix = record.Field(at + 9),
Qth = record.Field(at + 10),
Name = record.Field(at + 11),
Comment = record.Field(at + 12),
ReceivedNumber = record.Number(at + 13),
Section = record.Field(at + 14),
Precedence = record.Field(at + 15),
Check = record.Number(at + 16),
Zone = record.Number(at + 17),
SentNumber = record.Number(at + 18),
Points = record.Number(at + 19),
IsMultiplier1 = record.Flag(at + 20),
IsMultiplier2 = record.Flag(at + 21),
Power = record.Field(at + 22),
WpxPrefix = record.Field(at + 24),
Exchange1 = record.Field(at + 25),
RadioNumber = Math.Max(1, record.Number(at + 26)),
Operator = record.Field(at + 27),
GridSquare = record.Field(at + 28),
ContestNumber = record.Number(at + 29),
IsMultiplier3 = record.Flag(at + 30),
MiscText = record.Field(at + 31),
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.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);
}