Run the action macros in a function key message
A function key message was read as text and nothing else, so a message file
written for N1MM lost half of what it said: {WIPE}, {LOG}, {RUN} and the rest
expanded to nothing and the key only sent characters.
Reading a message now produces a plan rather than a string. MessagePlan.Read
walks the template once and sorts what it finds into three parts: the actions
that run before anything goes out, the text that goes on the air, and the
actions that wait until it has gone. N1MM's {END} decides which side an action
falls on, and text after {END} is dropped, because the message is over by then.
So TU {LOG}{END}{WIPE} logs the contact, sends TU, and clears the boxes when the
key has finished sending.
The actions that are carried out: {WIPE}, {LOG}, {RUN}, {S&P}, {SPACE},
{SPOTME}, {STOPTX}, {FREQUP}, {FREQDN}, {PGUP}, {PGDN}, {JUMPRX}, {JUMPRXTX},
{CTRLF1} to {CTRLF12} for sending on the other radio, {TELNET sh/dx} for a
command to the cluster node, and {OTRSP TX2} for one to the SO2R box. The last
one wanted a way to send a command straight to the box, so So2rBox grew one.
The frequency steps are two settings, with N1MM's own two numbers behind them.
Waiting on the keyer is the part worth reading twice. What follows {END} runs
when the keyer says the message has gone out, which is the only honest moment:
timing it from the length of the text is a guess that goes wrong exactly when
the contest is busy. A keyer that does not report completion is taken at its
word as soon as the text is handed over, and one that goes quiet is given two
minutes before the actions run anyway, because never running them is worse.
Running it against a fake cwdaemon and a fake node showed the message text on
the keyer, the telnet command at the node, RUN lighting up before the message
and the boxes clearing after it. That is also what showed the entry window was
not redrawing between the actions and the message.
What is still read and passed over is written down: the CAT, audio, rotator,
stereo and call-stacking families, the digital TNC macros, and the CW keyer
characters < > ~ ] [ + = , which belong to the keyer rather than to the
expander. {EXCHSENT} waits on ESM, which is next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
namespace Nonemm.Session;
|
||||
|
||||
/// One action macro out of a function key message, with whatever the operator
|
||||
/// wrote after the command: the telnet command, the OTRSP command, or the
|
||||
/// function key number.
|
||||
public sealed record MessageAction(MessageCommand Command, string Argument = "");
|
||||
@@ -0,0 +1,50 @@
|
||||
namespace Nonemm.Session;
|
||||
|
||||
/// What an action macro in a function key message does. N1MM's action macros
|
||||
/// that this program has nothing to do — the CAT families, audio, rotators,
|
||||
/// call stacking — are not here; they are read and passed over.
|
||||
public enum MessageCommand
|
||||
{
|
||||
/// Clears the entry boxes, as Escape and N1MM's `{WIPE}` do.
|
||||
Wipe,
|
||||
|
||||
/// Logs the contact, as the Log It button does.
|
||||
Log,
|
||||
|
||||
Run,
|
||||
|
||||
SearchAndPounce,
|
||||
|
||||
/// The same as pressing space in an entry box: move to the next field.
|
||||
Space,
|
||||
|
||||
/// Puts this station on the cluster.
|
||||
SpotMe,
|
||||
|
||||
FrequencyUp,
|
||||
|
||||
FrequencyDown,
|
||||
|
||||
PageUp,
|
||||
|
||||
PageDown,
|
||||
|
||||
/// Stops sending, as Escape does mid-message.
|
||||
StopSending,
|
||||
|
||||
/// Sends its argument to the cluster node.
|
||||
Telnet,
|
||||
|
||||
/// Moves the keyboard to the other radio; `WithTransmit` moves the
|
||||
/// transmitter as well.
|
||||
JumpToOtherRadio,
|
||||
|
||||
JumpToOtherRadioWithTransmit,
|
||||
|
||||
/// Sends one of the other radio's function key messages. The argument is
|
||||
/// the key number, so `{CTRLF9}` sends F9 there.
|
||||
SendOnOtherRadio,
|
||||
|
||||
/// Sends its argument to the SO2R box.
|
||||
Otrsp,
|
||||
}
|
||||
@@ -38,14 +38,14 @@ public static class MessageExpander
|
||||
text.Append(template[at..]);
|
||||
break;
|
||||
}
|
||||
text.Append(Macro(template[(at + 1)..close], session, other));
|
||||
text.Append(TextMacro(template[(at + 1)..close], session, other) ?? "");
|
||||
at = close + 1;
|
||||
continue;
|
||||
}
|
||||
// the single-character macros, which take no braces
|
||||
if (c is '*' or '!' or '#')
|
||||
{
|
||||
text.Append(Single(c, session));
|
||||
text.Append(TextMacro(c, session));
|
||||
at++;
|
||||
continue;
|
||||
}
|
||||
@@ -55,7 +55,8 @@ public static class MessageExpander
|
||||
return text.ToString();
|
||||
}
|
||||
|
||||
private static string Single(char macro, RadioPosition session) => macro switch
|
||||
/// The macros that need no braces: `*`, `!` and `#`.
|
||||
public static string TextMacro(char macro, RadioPosition session) => macro switch
|
||||
{
|
||||
'*' => session.Me.Callsign,
|
||||
'!' => TheirCall(session),
|
||||
@@ -70,7 +71,9 @@ public static class MessageExpander
|
||||
? last.SentNumber.ToString(CultureInfo.InvariantCulture)
|
||||
: session.SentNumber.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
private static string Macro(string name, RadioPosition session, RadioPosition? other) =>
|
||||
/// What the macro stands for, or null when it is not a text macro — an
|
||||
/// action macro, or a name this program does not know.
|
||||
public static string? TextMacro(string name, RadioPosition session, RadioPosition? other) =>
|
||||
name.ToUpperInvariant() switch
|
||||
{
|
||||
"MYCALL" => session.Me.Callsign,
|
||||
@@ -102,7 +105,7 @@ public static class MessageExpander
|
||||
"RRMHZ" => Megahertz(session, Radio(session, other, 2)),
|
||||
"TIMESTAMP" => DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture),
|
||||
"TIME2" => DateTime.UtcNow.ToString("HHmm", CultureInfo.InvariantCulture),
|
||||
_ => "",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
/// The call being worked, or the last one logged when the box is empty,
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace Nonemm.Session;
|
||||
|
||||
/// What one function key message does: the actions that run before anything
|
||||
/// goes on the air, the text that goes on the air, and the actions that run
|
||||
/// once it has gone out.
|
||||
///
|
||||
/// N1MM's rule, which this follows: an action macro runs before the message is
|
||||
/// sent, unless it stands after `{END}`, and then it runs when the message has
|
||||
/// finished. Text after `{END}` is dropped, because the message is over.
|
||||
public sealed record MessagePlan(
|
||||
IReadOnlyList<MessageAction> Before,
|
||||
string Text,
|
||||
IReadOnlyList<MessageAction> After)
|
||||
{
|
||||
public static MessagePlan Read(string template, RadioPosition session, RadioPosition? other = null)
|
||||
{
|
||||
List<MessageAction> before = [];
|
||||
List<MessageAction> after = [];
|
||||
StringBuilder text = new();
|
||||
bool ended = false;
|
||||
int at = 0;
|
||||
while (at < template.Length)
|
||||
{
|
||||
char c = template[at];
|
||||
if (c == '{')
|
||||
{
|
||||
int close = template.IndexOf('}', at + 1);
|
||||
if (close < 0)
|
||||
{
|
||||
Add(text, template[at..], ended);
|
||||
break;
|
||||
}
|
||||
string name = template[(at + 1)..close];
|
||||
at = close + 1;
|
||||
if (Same(name, "END"))
|
||||
{
|
||||
ended = true;
|
||||
}
|
||||
else if (MessageExpander.TextMacro(name, session, other) is { } filled)
|
||||
{
|
||||
Add(text, filled, ended);
|
||||
}
|
||||
else if (Action(name) is { } action)
|
||||
{
|
||||
(ended ? after : before).Add(action);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c is '*' or '!' or '#')
|
||||
{
|
||||
Add(text, MessageExpander.TextMacro(c, session), ended);
|
||||
at++;
|
||||
continue;
|
||||
}
|
||||
Add(text, c.ToString(), ended);
|
||||
at++;
|
||||
}
|
||||
return new MessagePlan(before, text.ToString(), after);
|
||||
}
|
||||
|
||||
/// Null for a macro this program does not act on, which is then passed
|
||||
/// over rather than sent as text.
|
||||
private static MessageAction? Action(string name)
|
||||
{
|
||||
string trimmed = name.Trim();
|
||||
string upper = trimmed.ToUpperInvariant();
|
||||
if (Argument(trimmed, "TELNET") is { } command)
|
||||
{
|
||||
return new MessageAction(MessageCommand.Telnet, command);
|
||||
}
|
||||
if (Argument(trimmed, "OTRSP") is { } otrsp)
|
||||
{
|
||||
return new MessageAction(MessageCommand.Otrsp, otrsp);
|
||||
}
|
||||
if (upper.StartsWith("CTRLF", StringComparison.Ordinal)
|
||||
&& int.TryParse(upper[5..], NumberStyles.None, CultureInfo.InvariantCulture, out int key)
|
||||
&& key is >= 1 and <= 12)
|
||||
{
|
||||
return new MessageAction(
|
||||
MessageCommand.SendOnOtherRadio,
|
||||
key.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
return upper switch
|
||||
{
|
||||
"WIPE" => new MessageAction(MessageCommand.Wipe),
|
||||
"LOG" => new MessageAction(MessageCommand.Log),
|
||||
"RUN" => new MessageAction(MessageCommand.Run),
|
||||
"S&P" => new MessageAction(MessageCommand.SearchAndPounce),
|
||||
"SPACE" => new MessageAction(MessageCommand.Space),
|
||||
"SPOTME" => new MessageAction(MessageCommand.SpotMe),
|
||||
"FREQUP" => new MessageAction(MessageCommand.FrequencyUp),
|
||||
"FREQDN" => new MessageAction(MessageCommand.FrequencyDown),
|
||||
"PGUP" => new MessageAction(MessageCommand.PageUp),
|
||||
"PGDN" => new MessageAction(MessageCommand.PageDown),
|
||||
"STOPTX" => new MessageAction(MessageCommand.StopSending),
|
||||
"JUMPRX" => new MessageAction(MessageCommand.JumpToOtherRadio),
|
||||
"JUMPRXTX" => new MessageAction(MessageCommand.JumpToOtherRadioWithTransmit),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// `{TELNET sh/dx}` and `{OTRSP TX1}` carry their command after the name.
|
||||
private static string? Argument(string macro, string name) =>
|
||||
macro.StartsWith(name + " ", StringComparison.OrdinalIgnoreCase)
|
||||
? macro[(name.Length + 1)..].Trim()
|
||||
: null;
|
||||
|
||||
private static void Add(StringBuilder text, string what, bool ended)
|
||||
{
|
||||
if (!ended)
|
||||
{
|
||||
text.Append(what);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool Same(string a, string b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
Reference in New Issue
Block a user