Start the transmit pane again on every new message
The macro buttons went wrong at the end of a transmission, both faults in
DigitalEngineSender:
- The engine reports the transmitter drop on its own thread.
WhenTransmitChanged took the state lock, decided the message had ended,
released the lock, and only then called Buffer.Ended(), which clears
what has gone to the engine. A macro pressed on the last character got
through StartAsync in that gap and had already flushed its own text into
Sent, so Ended() wiped the new text off the pane while the engine
transmitted it. Ended() is now called inside the same lock.
- The pane only started again when the engine reported a drop. Two macros
in a row keep the transmitter up, so that report never came and Sent
grew with every press. Everything in Sent is locked, because it is in
the engine and cannot be taken back, so the whole pane became
read-only. TypeAhead.Started() drops the last message's sent text and
keeps what was typed ahead, and StartAsync calls it whenever it keys a
new transmission.
The rest of this commit is the digital transmit work these fixes sit on:
the pane as one coloured box, the sender's three keying states, the
type-ahead feeder paced by the clock with the engine's count as a brake,
{RX} flushing what is left in one piece, and the entry window's function
keys reading the digital macros on a digital mode.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
This commit is contained in:
@@ -35,13 +35,33 @@ public sealed partial class EntryWindow
|
||||
|
||||
/// The twelve keys for this radio, which are a different twelve while
|
||||
/// running and while searching, as N1MM's file holds them.
|
||||
///
|
||||
/// On a digital mode they come from the digital macros instead, which is
|
||||
/// where N1MM reads them from as well: its entry window loads the RTTYBTN
|
||||
/// set there rather than the CW file. Without this the keys held CW text
|
||||
/// with no `{TX}` in it, so pressing one fed the engine without keying the
|
||||
/// transmitter and nothing went on the air.
|
||||
private IReadOnlyList<FunctionKey> Keys() =>
|
||||
Messages
|
||||
.For(
|
||||
Logging?.Mode.Category ?? Core.ModeCategory.Cw,
|
||||
session.Settings.CwMessageFile,
|
||||
session.Settings.PhoneMessageFile)
|
||||
.Keys(Logging?.IsRunning ?? false);
|
||||
Logging?.Mode.Category == ModeCategory.Digital
|
||||
? DigitalKeys()
|
||||
: Messages
|
||||
.For(
|
||||
Logging?.Mode.Category ?? Core.ModeCategory.Cw,
|
||||
session.Settings.CwMessageFile,
|
||||
session.Settings.PhoneMessageFile)
|
||||
.Keys(Logging?.IsRunning ?? false);
|
||||
|
||||
/// The first ten digital macros, then Spot and Wipe. F11 and F12 are what
|
||||
/// they are in every other mode, so the labels say what the buttons do;
|
||||
/// the macros past the tenth are on the digital window's own buttons.
|
||||
private IReadOnlyList<FunctionKey> DigitalKeys()
|
||||
{
|
||||
IReadOnlyList<FunctionKey> macros = Messages.Digital(session.Settings.DigitalMessageFile).Buttons;
|
||||
List<FunctionKey> keys = [.. macros.Take(MessageFile.KeyCount - 2)];
|
||||
keys.Add(new FunctionKey("Spot", ""));
|
||||
keys.Add(new FunctionKey("Wipe", ""));
|
||||
return keys;
|
||||
}
|
||||
|
||||
/// Right-clicking a function key button opens the messages for the mode the
|
||||
/// radio is in, which is where import and export live too.
|
||||
@@ -140,7 +160,12 @@ public sealed partial class EntryWindow
|
||||
}
|
||||
if (plan.Text.Length == 0)
|
||||
{
|
||||
// a button that only acts: {WIPE}, {LOG}, {RUN}
|
||||
// a button that only acts: {WIPE}, {LOG}, {RUN}, or an {RX} on its
|
||||
// own, which has nothing to wait for
|
||||
foreach (MessageAction action in plan.After)
|
||||
{
|
||||
Run(action);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if ((through ?? Sender) is not { } keyer)
|
||||
@@ -170,7 +195,22 @@ public sealed partial class EntryWindow
|
||||
Status(e.Message);
|
||||
return;
|
||||
}
|
||||
_ = RunWhenSentAsync(keyer, after);
|
||||
// `{RX}` goes to the keyer as soon as the message is in it, which is
|
||||
// N1MM's ending: the text in one piece, the stop 400 ms behind it, and
|
||||
// the engine unkeys itself at the last character. Waiting for the
|
||||
// message to go out first leaves the engine empty, and a stop that
|
||||
// arrives there does nothing. Everything else after `{END}` still
|
||||
// waits.
|
||||
foreach (MessageAction action in after)
|
||||
{
|
||||
if (action.Command == MessageCommand.ReturnToReceive)
|
||||
{
|
||||
Run(action);
|
||||
}
|
||||
}
|
||||
_ = RunWhenSentAsync(
|
||||
keyer,
|
||||
[.. after.Where(action => action.Command != MessageCommand.ReturnToReceive)]);
|
||||
}
|
||||
|
||||
/// Waits for the keyer, turns the transmit light off, and runs whatever
|
||||
@@ -292,14 +332,30 @@ public sealed partial class EntryWindow
|
||||
Logging.Stack.Clear();
|
||||
ShowCallStack();
|
||||
break;
|
||||
// the digital engine keys itself while it has text to send, so
|
||||
// {TX} and {RX} only matter when a macro wants the transmitter
|
||||
// held open around what it sends
|
||||
// {TX} keys the transmitter and {RX} drops it. {RX} waits until
|
||||
// everything waiting has gone out, so it ends the transmission
|
||||
// rather than cutting it off
|
||||
case MessageCommand.StartTransmit:
|
||||
_ = session.Digital?.SetPttAsync(true);
|
||||
// through the keyer, which keys the engine before it feeds it
|
||||
// the message
|
||||
if (session.DigitalKeyer is { } keying)
|
||||
{
|
||||
keying.Transmit();
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = session.Digital?.SetPttAsync(true);
|
||||
}
|
||||
break;
|
||||
case MessageCommand.ReturnToReceive:
|
||||
_ = session.Digital?.ReturnToReceiveAsync();
|
||||
if (session.DigitalKeyer is { } digital)
|
||||
{
|
||||
digital.ReturnToReceiveWhenSent();
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = session.Digital?.ReturnToReceiveAsync();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user