Read split from the radio, and read two of them
Split. RadioState carries a transmit frequency, zero meaning the radio transmits where it listens, so split is not a second flag that can disagree with it. The contact stores it in N1MM's QSX column, the entry window shows 14008.00 followed by the transmit frequency, and the bandmap draws a red bar there. SetSplitAsync sets the frequency before turning split on, or a radio last split somewhere else transmits there. Every command now goes out with a + in front, asking rigctld for its extended answer: named fields ended by an RPRT line. The raw answer is bare values with no terminator, so the client had to know how many lines each command returns — there was a `command == "m"` special case for the one that returns two. Add a third such command and get the count wrong once, and every later answer is read against the wrong command for the rest of the session. RigctldReply parses the extended form and is tested on its own. A radio that cannot do split answers RPRT -11. That is an answer, not a broken connection, so the frequency and mode it did report still count. Two radios. Settings hold a list rather than one host and port, with the single radio an older settings file holds carried into it. Both radios are read and both show on the bandmap, the active one green and the other orange, but only the active one drives the entry window: the second radio moving must not drag the operator off the station being worked. Ctrl+Tab swaps, and a contact records which radio made it. This is not a full two-radio operating position — no second entry window, no alternating CQ, no audio switching. It is two radios read and logged correctly. Tested against a stand-in for rigctld over a real socket, and looked at under Xvfb with two fake radios, one of them split. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
using System.Globalization;
|
||||
using System.Net.Sockets;
|
||||
using Nonemm.Core;
|
||||
|
||||
@@ -24,15 +23,19 @@ public sealed class RigctldRadio : Radio
|
||||
public RigctldRadio(
|
||||
string host = "127.0.0.1",
|
||||
int port = 4532,
|
||||
int number = 1,
|
||||
TimeSpan? pollInterval = null,
|
||||
TimeSpan? retryInterval = null)
|
||||
{
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
Number = number;
|
||||
this.pollInterval = pollInterval ?? TimeSpan.FromMilliseconds(200);
|
||||
this.retryInterval = retryInterval ?? TimeSpan.FromSeconds(3);
|
||||
}
|
||||
|
||||
public int Number { get; }
|
||||
|
||||
public bool IsConnected { get; private set; }
|
||||
|
||||
public RadioState? State { get; private set; }
|
||||
@@ -49,6 +52,21 @@ public sealed class RigctldRadio : Radio
|
||||
public async Task SetModeAsync(Mode mode, CancellationToken cancellation = default) =>
|
||||
await SendAsync($"M {RigctldMode(mode)} 0", cancellation).ConfigureAwait(false);
|
||||
|
||||
/// The transmit frequency has to be set before split is turned on, or a
|
||||
/// radio that was last split somewhere else transmits there.
|
||||
public async Task SetSplitAsync(
|
||||
Frequency? transmitFrequency,
|
||||
CancellationToken cancellation = default)
|
||||
{
|
||||
if (transmitFrequency is null)
|
||||
{
|
||||
await SendAsync("S 0 VFOA", cancellation).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
await SendAsync($"I {transmitFrequency.Value.Hertz}", cancellation).ConfigureAwait(false);
|
||||
await SendAsync("S 1 VFOB", cancellation).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
stopping.Cancel();
|
||||
@@ -93,11 +111,16 @@ public sealed class RigctldRadio : Radio
|
||||
{
|
||||
while (!cancellation.IsCancellationRequested)
|
||||
{
|
||||
Frequency? frequency = ReadFrequency(await AskAsync("f", cancellation).ConfigureAwait(false));
|
||||
Mode? mode = ReadMode(await AskAsync("m", cancellation).ConfigureAwait(false));
|
||||
if (frequency is not null)
|
||||
RigctldReply frequency = await AskAsync("f", cancellation).ConfigureAwait(false);
|
||||
RigctldReply mode = await AskAsync("m", cancellation).ConfigureAwait(false);
|
||||
if (frequency.Number("Frequency") is > 0 and long hertz)
|
||||
{
|
||||
RadioState state = new(frequency.Value, mode ?? State?.Mode ?? Modes.Cw);
|
||||
RadioState state = new(
|
||||
Frequency.FromHertz(hertz),
|
||||
Modes.Parse(mode.Value("Mode") ?? "") ?? State?.Mode ?? Modes.Cw)
|
||||
{
|
||||
TransmitFrequency = await TransmitFrequencyAsync(cancellation).ConfigureAwait(false),
|
||||
};
|
||||
if (state != State)
|
||||
{
|
||||
State = state;
|
||||
@@ -108,7 +131,25 @@ public sealed class RigctldRadio : Radio
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<string>> AskAsync(string command, CancellationToken cancellation)
|
||||
/// Zero when the radio is not split, and when it cannot tell us.
|
||||
private async Task<Frequency> TransmitFrequencyAsync(CancellationToken cancellation)
|
||||
{
|
||||
RigctldReply split = await AskAsync("s", cancellation).ConfigureAwait(false);
|
||||
if (!split.IsOk || split.Number("Split") is not 1)
|
||||
{
|
||||
return Frequency.Zero;
|
||||
}
|
||||
RigctldReply transmit = await AskAsync("i", cancellation).ConfigureAwait(false);
|
||||
return transmit.Number("TX Frequency") is > 0 and long hertz
|
||||
? Frequency.FromHertz(hertz)
|
||||
: Frequency.Zero;
|
||||
}
|
||||
|
||||
/// Every command goes out with a `+` in front, which asks rigctld for the
|
||||
/// extended answer: named fields ended by an RPRT line. Without the
|
||||
/// terminator a client has to know how many lines each command returns, and
|
||||
/// one wrong count desynchronises the connection for good.
|
||||
private async Task<RigctldReply> AskAsync(string command, CancellationToken cancellation)
|
||||
{
|
||||
await writing.WaitAsync(cancellation).ConfigureAwait(false);
|
||||
try
|
||||
@@ -117,21 +158,18 @@ public sealed class RigctldRadio : Radio
|
||||
{
|
||||
throw new IOException("the connection to rigctld is not open");
|
||||
}
|
||||
await writer.WriteAsync(command + "\n").ConfigureAwait(false);
|
||||
await writer.WriteAsync("+" + command + "\n").ConfigureAwait(false);
|
||||
List<string> lines = [];
|
||||
// rigctld answers one value per line and ends with RPRT on an error
|
||||
string? line = await reader.ReadLineAsync(cancellation).ConfigureAwait(false)
|
||||
?? throw new IOException($"rigctld closed the connection during '{command}'");
|
||||
lines.Add(line);
|
||||
if (command == "m")
|
||||
while (true)
|
||||
{
|
||||
string? passband = await reader.ReadLineAsync(cancellation).ConfigureAwait(false);
|
||||
if (passband is not null)
|
||||
string line = await reader.ReadLineAsync(cancellation).ConfigureAwait(false)
|
||||
?? throw new IOException($"rigctld closed the connection during '{command}'");
|
||||
lines.Add(line);
|
||||
if (line.StartsWith("RPRT", StringComparison.Ordinal))
|
||||
{
|
||||
lines.Add(passband);
|
||||
return RigctldReply.Parse(lines);
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -145,24 +183,14 @@ public sealed class RigctldRadio : Radio
|
||||
{
|
||||
return;
|
||||
}
|
||||
await writing.WaitAsync(cancellation).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (writer is null || reader is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await writer.WriteAsync(command + "\n").ConfigureAwait(false);
|
||||
await reader.ReadLineAsync(cancellation).ConfigureAwait(false);
|
||||
await AskAsync(command, cancellation).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e) when (e is IOException or SocketException)
|
||||
{
|
||||
SetConnected(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
writing.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetConnected(bool connected)
|
||||
@@ -174,16 +202,6 @@ public sealed class RigctldRadio : Radio
|
||||
}
|
||||
}
|
||||
|
||||
private static Frequency? ReadFrequency(IReadOnlyList<string> answer) =>
|
||||
answer.Count > 0 &&
|
||||
long.TryParse(answer[0].Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out long hertz) &&
|
||||
hertz > 0
|
||||
? Frequency.FromHertz(hertz)
|
||||
: null;
|
||||
|
||||
private static Mode? ReadMode(IReadOnlyList<string> answer) =>
|
||||
answer.Count > 0 ? Modes.Parse(answer[0].Trim()) : null;
|
||||
|
||||
/// hamlib names the modes CW, USB, LSB, RTTY, FM, AM and PKTUSB.
|
||||
private static string RigctldMode(Mode mode) => mode.Name switch
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user