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 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); }