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:
2026-08-27 16:49:24 +00:00
parent 43be3816c3
commit 1185faed78
22 changed files with 714 additions and 208 deletions

View File

@@ -0,0 +1,69 @@
using System.Text;
namespace Nonemm.Rig.Tests;
public class OtrspBoxTests
{
private static string Written(MemoryStream stream) =>
Encoding.ASCII.GetString(stream.ToArray());
[Fact]
public async Task PointingTheTransmitterNamesTheRadio()
{
MemoryStream sent = new();
using OtrspBox box = new(sent);
await box.SetTransmitAsync(2);
Assert.Equal("TX2\r", Written(sent));
}
[Fact]
public async Task ListeningToBothRadiosAddsTheStereoFlag()
{
MemoryStream sent = new();
using OtrspBox box = new(sent);
await box.SetReceiveAsync(1, bothRadios: true);
Assert.Equal("RX1S\r", Written(sent));
}
/// A box works relays. Saying the same thing again would rattle them for
/// nothing, so it is not said.
[Fact]
public async Task TheSameCommandIsNotSentTwice()
{
MemoryStream sent = new();
using OtrspBox box = new(sent);
await box.SetTransmitAsync(1);
await box.SetTransmitAsync(1);
await box.SetTransmitAsync(2);
Assert.Equal("TX1\rTX2\r", Written(sent));
}
[Fact]
public async Task TransmitAndReceiveAreTrackedApart()
{
MemoryStream sent = new();
using OtrspBox box = new(sent);
await box.SetTransmitAsync(1);
await box.SetReceiveAsync(1);
Assert.Equal("TX1\rRX1\r", Written(sent));
}
[Fact]
public async Task AnAuxiliaryCodeIsSentAsTwoDigits()
{
MemoryStream sent = new();
using OtrspBox box = new(sent);
await box.SetAuxiliaryAsync(2, 3);
Assert.Equal("AUX203\r", Written(sent));
}
}