Two entry windows and an SO2R box
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>
This commit is contained in:
@@ -15,30 +15,54 @@ namespace Nonemm.App.Windows;
|
||||
public sealed partial class EntryWindow : Window
|
||||
{
|
||||
private readonly AppSession session;
|
||||
private readonly int radioNumber;
|
||||
private EntryWindow? secondRadio;
|
||||
private readonly List<TextBox> boxes = [];
|
||||
private readonly DispatcherTimer clock = new() { Interval = TimeSpan.FromSeconds(1) };
|
||||
private readonly Dictionary<Type, Window> openWindows = [];
|
||||
private bool updating;
|
||||
|
||||
public EntryWindow(AppSession session)
|
||||
public EntryWindow(AppSession session, int radioNumber = 1)
|
||||
{
|
||||
this.session = session;
|
||||
this.radioNumber = radioNumber;
|
||||
InitializeComponent();
|
||||
BuildFunctionKeys();
|
||||
session.Changed += (_, _) => Dispatcher.UIThread.Post(Refresh);
|
||||
session.ContestChanged += (_, _) => Dispatcher.UIThread.Post(BuildEntryBoxes);
|
||||
session.ContestChanged += (_, _) => Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
BuildEntryBoxes();
|
||||
ShowSecondRadio();
|
||||
});
|
||||
clock.Tick += (_, _) => UpdateClock();
|
||||
clock.Start();
|
||||
session.ActiveRadioChanged += (_, number) =>
|
||||
Dispatcher.UIThread.Post(() => FollowActiveRadio(number));
|
||||
// only the window for radio 1 opens the log and the connections; the
|
||||
// second window is another view of the same session
|
||||
Opened += (_, _) => Reopen();
|
||||
if (radioNumber > 1)
|
||||
{
|
||||
Title = "Nonemm — radio 2";
|
||||
MainMenu.IsVisible = false;
|
||||
}
|
||||
AddHandler(KeyDownEvent, OnWindowKeyDown, RoutingStrategies.Tunnel);
|
||||
}
|
||||
|
||||
|
||||
private LoggingSession? Logging => session.Logging;
|
||||
/// Resolved every time rather than held, because opening a contest builds
|
||||
/// new positions.
|
||||
private RadioPosition? Logging =>
|
||||
session.Positions.FirstOrDefault(p => p.RadioNumber == radioNumber);
|
||||
|
||||
/// Reopens the database and contest the operator was last in.
|
||||
private void Reopen()
|
||||
{
|
||||
if (radioNumber > 1)
|
||||
{
|
||||
BuildEntryBoxes();
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
int contestNumber = session.Settings.ContestNumber;
|
||||
@@ -57,6 +81,42 @@ public sealed partial class EntryWindow : Window
|
||||
}
|
||||
StartConnections();
|
||||
BuildEntryBoxes();
|
||||
ShowSecondRadio();
|
||||
}
|
||||
|
||||
/// A second radio gets its own entry window: the two share one log and one
|
||||
/// score, but what is typed on each belongs to that radio.
|
||||
private void ShowSecondRadio()
|
||||
{
|
||||
if (radioNumber > 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
bool wanted = session.Positions.Count > 1;
|
||||
if (wanted && secondRadio is null)
|
||||
{
|
||||
Title = "Nonemm — radio 1";
|
||||
secondRadio = new EntryWindow(session, 2);
|
||||
secondRadio.Closed += (_, _) => secondRadio = null;
|
||||
secondRadio.Show(this);
|
||||
}
|
||||
else if (!wanted && secondRadio is not null)
|
||||
{
|
||||
Title = "Nonemm";
|
||||
secondRadio.Close();
|
||||
secondRadio = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// The keyboard follows the operator to the other radio.
|
||||
private void FollowActiveRadio(int number)
|
||||
{
|
||||
if (number != radioNumber || Logging is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Activate();
|
||||
boxes.ElementAtOrDefault(Logging.Entry.Focus)?.Focus();
|
||||
}
|
||||
|
||||
/// Brings up whatever the operator had connected last time. Each is
|
||||
@@ -94,6 +154,10 @@ public sealed partial class EntryWindow : Window
|
||||
{
|
||||
yield return ("keyer", session.ApplyKeyerSettings);
|
||||
}
|
||||
if (session.Settings.So2rBoxPort.Length > 0)
|
||||
{
|
||||
yield return ("SO2R box", session.ApplySo2rSettings);
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildEntryBoxes()
|
||||
@@ -175,10 +239,15 @@ public sealed partial class EntryWindow : Window
|
||||
SyncBoxes();
|
||||
boxes[0].Focus();
|
||||
break;
|
||||
case Key.Tab when e.KeyModifiers.HasFlag(KeyModifiers.Control)
|
||||
&& e.KeyModifiers.HasFlag(KeyModifiers.Shift):
|
||||
e.Handled = true;
|
||||
session.ToggleListenToBoth();
|
||||
Status(session.IsListeningToBoth ? "both radios" : $"radio {session.ActiveRadioNumber}");
|
||||
break;
|
||||
case Key.Tab when e.KeyModifiers.HasFlag(KeyModifiers.Control):
|
||||
e.Handled = true;
|
||||
session.SwapRadio();
|
||||
Status($"radio {session.ActiveRadioNumber}");
|
||||
break;
|
||||
case Key.Tab when FocusedIsEntryBox():
|
||||
e.Handled = true;
|
||||
@@ -392,6 +461,8 @@ public sealed partial class EntryWindow : Window
|
||||
return;
|
||||
}
|
||||
string text = MessageExpander.Expand(template, Logging);
|
||||
// the box has to point at this radio before the key does
|
||||
_ = session.PointTransmitAtAsync(radioNumber);
|
||||
Status($"sending {text}");
|
||||
_ = SendAsync(text);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user