Add the Broadcast Data dialog and send N1MM's score breakdown
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Nonemm.Network;
|
||||
|
||||
/// What `RadioInfo` says about one radio. Frequencies are in hertz here and go
|
||||
/// out in the units of ten hertz N1MM uses.
|
||||
public sealed record RadioReport(
|
||||
int RadioNumber,
|
||||
long Frequency,
|
||||
long TransmitFrequency,
|
||||
string Mode,
|
||||
string StationCall,
|
||||
string OperatorCall,
|
||||
bool IsRunning,
|
||||
int FocusRadioNumber,
|
||||
bool IsSplit,
|
||||
int ActiveRadioNumber,
|
||||
bool IsTransmitting,
|
||||
string RadioName,
|
||||
bool IsConnected);
|
||||
|
||||
/// One spot as `spot` carries it. The frequency is in hertz.
|
||||
public sealed record SpotReport(
|
||||
string Call,
|
||||
long Frequency,
|
||||
string Spotter,
|
||||
DateTime AtUtc,
|
||||
string Mode,
|
||||
string Comment,
|
||||
string Status);
|
||||
|
||||
/// One row of the score as `dynamicresults` carries it. `Band` is the number
|
||||
/// N1MM stores for the band, and `Mode` the mode of the contacts as logged;
|
||||
/// both are turned into the labels of the message when it is written.
|
||||
public sealed record ScoreReportRow(
|
||||
double Band,
|
||||
string Mode,
|
||||
int Qsos,
|
||||
IReadOnlyList<int> Multipliers,
|
||||
int Points);
|
||||
|
||||
/// What `dynamicresults` says about the entry.
|
||||
public sealed record ScoreReport(
|
||||
string Contest,
|
||||
string Call,
|
||||
string Operators,
|
||||
string Power,
|
||||
string Assisted,
|
||||
string Transmitter,
|
||||
string OperatorCategory,
|
||||
string Bands,
|
||||
string ModeCategory,
|
||||
string Overlay,
|
||||
string Station,
|
||||
string Club,
|
||||
string Country,
|
||||
int CqZone,
|
||||
int IaruZone,
|
||||
string ArrlSection,
|
||||
string State,
|
||||
string Grid,
|
||||
IReadOnlyList<string> MultiplierNames,
|
||||
IReadOnlyList<ScoreReportRow> Rows,
|
||||
long Score,
|
||||
DateTime AtUtc);
|
||||
|
||||
/// The messages N1MM sends to other programs besides the contacts: `AppInfo`,
|
||||
/// `RadioInfo`, `spot` and `dynamicresults`. The element names are N1MM's.
|
||||
public static class BroadcastMessages
|
||||
{
|
||||
private const long FrequencyUnit = 10;
|
||||
|
||||
private const string TimeFormat = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
public static string AppInfo(
|
||||
string databaseName,
|
||||
int contestNumber,
|
||||
string contestName,
|
||||
string stationName,
|
||||
string myCall) =>
|
||||
new XElement(
|
||||
"AppInfo",
|
||||
new XElement("app", "Nonemm"),
|
||||
new XElement("dbname", databaseName),
|
||||
new XElement("contestnr", contestNumber),
|
||||
new XElement("contestname", contestName),
|
||||
new XElement("StationName", stationName),
|
||||
new XElement("mycall", myCall)).ToString();
|
||||
|
||||
/// Fields this program has no value for are sent with the value N1MM sends
|
||||
/// when it has none, so a reader that expects all of them finds them.
|
||||
public static string Radio(RadioReport radio, string stationName) =>
|
||||
new XElement(
|
||||
"RadioInfo",
|
||||
new XElement("app", "Nonemm"),
|
||||
new XElement("StationName", stationName),
|
||||
new XElement("RadioNr", radio.RadioNumber),
|
||||
new XElement("Freq", radio.Frequency / FrequencyUnit),
|
||||
new XElement("TXFreq", radio.TransmitFrequency / FrequencyUnit),
|
||||
new XElement("Mode", radio.Mode),
|
||||
new XElement("mycall", radio.StationCall),
|
||||
new XElement("OpCall", radio.OperatorCall),
|
||||
new XElement("IsRunning", Flag(radio.IsRunning)),
|
||||
new XElement("FocusEntry", 0),
|
||||
new XElement("EntryWindowHwnd", 0),
|
||||
new XElement("Antenna", 0),
|
||||
new XElement("Rotors", ""),
|
||||
new XElement("FocusRadioNr", radio.FocusRadioNumber),
|
||||
new XElement("IsStereo", Flag(false)),
|
||||
new XElement("IsSplit", Flag(radio.IsSplit)),
|
||||
new XElement("ActiveRadioNr", radio.ActiveRadioNumber),
|
||||
new XElement("IsTransmitting", Flag(radio.IsTransmitting)),
|
||||
new XElement("FunctionKeyCaption", ""),
|
||||
new XElement("RadioName", radio.RadioName),
|
||||
new XElement("AuxAntSelected", 0),
|
||||
new XElement("AuxAntSelectedName", ""),
|
||||
new XElement("IsConnected", Flag(radio.IsConnected))).ToString();
|
||||
|
||||
/// `action` is `add` or `delete`. N1MM leaves the comment and status off a
|
||||
/// delete.
|
||||
public static string Spot(SpotReport spot, string action, string stationName)
|
||||
{
|
||||
XElement root = new(
|
||||
"spot",
|
||||
new XElement("app", "Nonemm"),
|
||||
new XElement("StationName", stationName),
|
||||
new XElement("dxcall", spot.Call),
|
||||
new XElement("frequency", (spot.Frequency / 1000.0).ToString("0.0", CultureInfo.InvariantCulture)),
|
||||
new XElement("spottercall", spot.Spotter),
|
||||
new XElement("timestamp", spot.AtUtc.ToString(TimeFormat, CultureInfo.InvariantCulture)),
|
||||
new XElement("action", action),
|
||||
new XElement("mode", spot.Mode));
|
||||
if (action == "add")
|
||||
{
|
||||
root.Add(new XElement("comment", spot.Comment));
|
||||
root.Add(new XElement("status", spot.Status));
|
||||
root.Add(new XElement("statuslist", spot.Status));
|
||||
}
|
||||
return root.ToString();
|
||||
}
|
||||
|
||||
/// Written the way N1MM writes it, line by line with tabs: the layout and
|
||||
/// the order of the elements are what a program reading it relies on.
|
||||
public static string Score(ScoreReport report)
|
||||
{
|
||||
StringBuilder text = new("<?xml version=\"1.0\"?>\r\n<dynamicresults>\r\n");
|
||||
text.Append("\t<contest>").Append(Escape(report.Contest)).Append("</contest>\r\n");
|
||||
text.Append("\t<call>").Append(Escape(report.Call.Trim())).Append("</call>\r\n");
|
||||
text.Append("\t<ops>").Append(Escape(report.Operators)).Append("</ops>\r\n");
|
||||
text.Append("\t<class power=\"").Append(Escape(report.Power));
|
||||
text.Append("\" assisted = \"").Append(Escape(report.Assisted));
|
||||
text.Append("\" transmitter=\"").Append(Escape(report.Transmitter));
|
||||
text.Append("\" ops=\"").Append(Escape(ScoreLabels.OperatorCategory(report.OperatorCategory)));
|
||||
text.Append("\" bands=\"").Append(Escape(report.Bands));
|
||||
text.Append("\" mode=\"").Append(Escape(report.ModeCategory));
|
||||
text.Append("\" overlay=\"").Append(Escape(report.Overlay));
|
||||
text.Append("\" station=\"").Append(Escape(report.Station));
|
||||
text.Append("\" ></class>\r\n");
|
||||
text.Append("\t<club>").Append(Escape(report.Club)).Append("</club>\r\n");
|
||||
text.Append("\t<qth>\r\n");
|
||||
text.Append("\t\t<dxcccountry>").Append(Escape(report.Country)).Append("</dxcccountry>\r\n");
|
||||
text.Append("\t\t<cqzone>").Append(report.CqZone).Append("</cqzone>\r\n");
|
||||
text.Append("\t\t<iaruzone>").Append(report.IaruZone).Append("</iaruzone>\r\n");
|
||||
if (report.ArrlSection.Length > 0)
|
||||
{
|
||||
text.Append("\t\t<arrlsection>").Append(Escape(report.ArrlSection)).Append("</arrlsection>\r\n");
|
||||
}
|
||||
if (report.State.Length > 0)
|
||||
{
|
||||
text.Append("\t\t<stprvoth>").Append(Escape(report.State)).Append("</stprvoth>\r\n");
|
||||
}
|
||||
if (report.Grid.Length is 4 or 6)
|
||||
{
|
||||
text.Append($"\t\t<grid{report.Grid.Length}>").Append(Escape(report.Grid)).Append($"</grid{report.Grid.Length}>\r\n");
|
||||
}
|
||||
text.Append("\t</qth>\r\n\t<breakdown>\r\n");
|
||||
foreach (ScoreReportRow row in report.Rows)
|
||||
{
|
||||
string band = ScoreLabels.Band(row.Band);
|
||||
string mode = ScoreLabels.Mode(row.Mode);
|
||||
text.Append($"\t\t<qso band=\"{band}\" mode=\"{mode}\">{row.Qsos}</qso>\r\n");
|
||||
for (int at = 0; at < report.MultiplierNames.Count; at++)
|
||||
{
|
||||
string type = Escape(ScoreLabels.MultiplierType(report.MultiplierNames[at]));
|
||||
int count = at < row.Multipliers.Count ? row.Multipliers[at] : 0;
|
||||
text.Append($"\t\t<mult band=\"{band}\" mode=\"{mode}\" type=\"{type}\">{count}</mult>\r\n");
|
||||
}
|
||||
text.Append($"\t\t<point band=\"{band}\" mode=\"{mode}\">{row.Points}</point>\r\n");
|
||||
}
|
||||
text.Append("\t</breakdown>\r\n");
|
||||
text.Append("\t<score>").Append(report.Score).Append("</score>\r\n");
|
||||
text.Append("\t<timestamp>")
|
||||
.Append(report.AtUtc.ToString(TimeFormat, CultureInfo.InvariantCulture))
|
||||
.Append("</timestamp>\r\n</dynamicresults>\r\n");
|
||||
return text.ToString();
|
||||
}
|
||||
|
||||
/// N1MM writes these values as they are, so a club called `A&B` makes a
|
||||
/// message no parser reads. They are escaped here.
|
||||
private static string Escape(string value) => System.Security.SecurityElement.Escape(value) ?? "";
|
||||
|
||||
/// .NET's own text for a boolean, which is what N1MM writes.
|
||||
private static string Flag(bool value) => value ? "True" : "False";
|
||||
}
|
||||
Reference in New Issue
Block a user