LoggingSession mixed two things: the contest, which one station has one of, and what the operator is typing, which belongs to a radio. It is now ContestSession and RadioPosition. Two positions share one log, one score and one run of serial numbers, so a station worked on radio 1 is a dupe on radio 2. Frequency, mode, split, what is typed and whether you are running belong to each radio on its own. A second radio opens a second entry window. It has no menu of its own: it is another view of the same contest, not another program. Only the window for radio 1 opens the database and the connections. Ctrl+Tab moves the operator to the other radio and the keyboard follows. Ctrl+Shift+Tab puts both radios in the headphones. OtrspBox speaks OTRSP to an SO2R box over a serial port: TX1/TX2 for the key, RX1/RX2 and RX1S for the headphones, AUXnnn for an output line. It follows the active radio, and every message points the box at this radio before keying, so a message cannot go out of the radio the operator has just left. A command that would change nothing is not sent, because the box works relays; N1MM does the same. The box takes a Stream, so what it sends is tested without a serial port. Still missing: alternating CQ, and voice keying on the second radio. Looked at under Xvfb with two fake radios, one of them split: both windows up, radio 1 showing 14008.00 into 14020.0, radio 2 on 14030.00, Ctrl+Tab moving the keyboard between them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
154 lines
5.5 KiB
C#
154 lines
5.5 KiB
C#
using Nonemm.Contests;
|
|
using Nonemm.Core;
|
|
using Nonemm.Core.Country;
|
|
using Nonemm.Storage;
|
|
|
|
namespace Nonemm.Session;
|
|
|
|
/// One radio and what the operator is typing on it. A single-radio station has
|
|
/// one of these; an SO2R station has two, sharing one `ContestSession` and so
|
|
/// one log, one score and one run of serial numbers.
|
|
public sealed class RadioPosition
|
|
{
|
|
public RadioPosition(ContestSession session, int radioNumber = 1)
|
|
{
|
|
Session = session;
|
|
RadioNumber = radioNumber;
|
|
Entry = new EntryFields(session.Contest.ExchangeFieldsFor(session.Me));
|
|
}
|
|
|
|
public ContestSession Session { get; }
|
|
|
|
/// 1 or 2. Contacts record which radio made them.
|
|
public int RadioNumber { get; }
|
|
|
|
public EntryFields Entry { get; }
|
|
|
|
/// With no radio connected this stays where it was last typed.
|
|
public Frequency Frequency { get; private set; } = Bands.Band20M.Low;
|
|
|
|
/// Where the radio transmits when working split, and zero when it is not.
|
|
public Frequency TransmitFrequency { get; private set; } = Frequency.Zero;
|
|
|
|
public Mode Mode { get; private set; } = Modes.Cw;
|
|
|
|
/// True while the operator is calling CQ on this radio rather than
|
|
/// searching. Each radio runs or searches on its own.
|
|
public bool IsRunning { get; set; }
|
|
|
|
public Contest Contest => Session.Contest;
|
|
|
|
public ContestInstance Instance => Session.Instance;
|
|
|
|
public StationInfo Me => Session.Me;
|
|
|
|
public ContestLog Log => Session.Log;
|
|
|
|
public QsoEditor Editor => Session.Editor;
|
|
|
|
public int SentNumber => Session.SentNumber;
|
|
|
|
public void Tune(Frequency frequency, Mode? mode = null, Frequency? transmitFrequency = null)
|
|
{
|
|
Frequency = frequency;
|
|
Mode = mode ?? Mode;
|
|
TransmitFrequency = transmitFrequency ?? TransmitFrequency;
|
|
Session.NotifyChanged();
|
|
}
|
|
|
|
/// What the log says about what is typed now. Null when there is no call to
|
|
/// judge yet.
|
|
public Verdict? Verdict()
|
|
{
|
|
string call = Entry.Call.Trim();
|
|
return call.Length == 0 ? null : Log.Judge(BuildQso(call));
|
|
}
|
|
|
|
public CountryLookup? Country() =>
|
|
Entry.Call.Trim().Length == 0 ? null : Session.Countries?.Find(Entry.Call.Trim());
|
|
|
|
/// Every earlier contact with the call being typed, newest first.
|
|
public IReadOnlyList<Qso> WorkedBefore() =>
|
|
Entry.Call.Trim().Length == 0 ? [] : Log.WorkedBefore(Entry.Call.Trim());
|
|
|
|
/// The frequency typed into the callsign box, if that is what it holds.
|
|
public Frequency? PendingQsy() => FrequencyEntry.Parse(Entry.Call);
|
|
|
|
/// Logs what is typed and clears the entry. Throws when the entry is not
|
|
/// complete, so a caller that has not checked cannot lose an exchange.
|
|
public Qso LogContact()
|
|
{
|
|
if (!Entry.IsComplete)
|
|
{
|
|
throw new InvalidOperationException($"the exchange for {Entry.Call} is not complete");
|
|
}
|
|
Qso stored = Session.Add(BuildQso(Entry.Call.Trim()));
|
|
Entry.Clear();
|
|
return stored;
|
|
}
|
|
|
|
public void Wipe()
|
|
{
|
|
Entry.Clear();
|
|
Session.NotifyChanged();
|
|
}
|
|
|
|
/// The report an operator sends without thinking about it: 599 on CW and
|
|
/// digital modes, 59 on phone.
|
|
public string DefaultReport() => Mode.Category switch
|
|
{
|
|
ModeCategory.Cw => "599",
|
|
ModeCategory.Phone => "59",
|
|
_ => "599",
|
|
};
|
|
|
|
private Qso BuildQso(string call)
|
|
{
|
|
Qso qso = new()
|
|
{
|
|
Id = Qso.NewId(),
|
|
TimestampUtc = DateTime.UtcNow.AddTicks(-(DateTime.UtcNow.Ticks % TimeSpan.TicksPerSecond)),
|
|
Call = Callsign.Parse(call),
|
|
Frequency = Frequency,
|
|
QsxFrequency = TransmitFrequency,
|
|
Mode = Mode,
|
|
RadioNumber = RadioNumber,
|
|
ContestName = Contest.Name,
|
|
ContestNumber = Instance.ContestNumber,
|
|
SentReport = DefaultReport(),
|
|
SentNumber = SentNumber,
|
|
Operator = Session.Operator.Length > 0 ? Session.Operator : Me.Callsign,
|
|
IsRunQso = IsRunning,
|
|
};
|
|
return ApplyExchange(CountryFields.Apply(qso, Session.Countries));
|
|
}
|
|
|
|
private Qso ApplyExchange(Qso qso)
|
|
{
|
|
foreach (ExchangeField field in Entry.Exchange)
|
|
{
|
|
string value = Entry.ValueOf(field.Slot).Trim();
|
|
qso = field.Slot switch
|
|
{
|
|
ExchangeSlot.ReceivedReport => qso with { ReceivedReport = value },
|
|
ExchangeSlot.SerialNumber => qso with { ReceivedNumber = Number(value) },
|
|
ExchangeSlot.Zone => qso with { Zone = Number(value) },
|
|
ExchangeSlot.Section => qso with { Section = value.ToUpperInvariant() },
|
|
ExchangeSlot.Check => qso with { Check = Number(value) },
|
|
ExchangeSlot.Precedence => qso with { Precedence = value.ToUpperInvariant() },
|
|
ExchangeSlot.Exchange1 => qso with { Exchange1 = value.ToUpperInvariant() },
|
|
ExchangeSlot.MiscText => qso with { MiscText = value.ToUpperInvariant() },
|
|
ExchangeSlot.Name => qso with { Name = value },
|
|
ExchangeSlot.Qth => qso with { Qth = value },
|
|
ExchangeSlot.GridSquare => qso with { GridSquare = value.ToUpperInvariant() },
|
|
ExchangeSlot.Power => qso with { Power = value },
|
|
ExchangeSlot.Comment => qso with { Comment = value },
|
|
_ => qso,
|
|
};
|
|
}
|
|
return qso;
|
|
}
|
|
|
|
private static int Number(string value) => int.TryParse(value, out int parsed) ? parsed : 0;
|
|
}
|