Let Enter send the message the contact has got to

ESM is how most people run N1MM, and it was not here at all — not in the code,
not even in the list of what is missing. Config ▸ ESM turns it on and it stays
on between runs.

The decision is a table in N1MM's function-key documentation, and it is written
here as one: the state of the callsign and exchange boxes, whether the station
is running or searching, and whether the call and the exchange have gone out
already, give the function keys Enter sends and whether the contact is logged
after them. Esm.Decide is that table and nothing else, so it is tested against
every row rather than by clicking.

Searching: type a call, Enter sends your call, space moves to the exchange, and
once the exchange is filled in Enter sends yours and logs. Running: Enter calls
CQ, a call in the box makes Enter send his call and the exchange, and the next
Enter ends the contact and logs it. A dupe gets QSO B4 while running and nothing
while searching, unless dupes are worked, which is what N1MM recommends and what
this does out of the box. F1 puts a searching station into run mode.

The entry window highlights the keys Enter would send next, so what is about to
happen is on the screen rather than in the operator's head. `=` sends whatever
Enter last sent. Escape and F12 put ESM back to the start of a contact.

Send Corrected Call is in as well: copy SM3AB, send it, fix it to SM3ABC, and
the message that ends the contact goes out as "SM3ABC TU DL1ABC". That is why
N1MM's documentation says to put ! in F5 rather than {CALL}, so the default F5
message is now ! . F6 is QSO B4, which is the message ESM sends a dupe; it used
to repeat the exchange, which no part of the table asks for.

{EXCHSENT} means something again, now that there is an ESM state for it to set.

Worked end to end against a fake cwdaemon: a search-and-pounce contact logged in
four keystrokes, a run contact in three, both with the right text on the keyer
in the right order, and the corrected call in front of the last message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 07:59:11 +00:00
parent 333d3b97c2
commit 0c440ed865
13 changed files with 458 additions and 13 deletions

View File

@@ -0,0 +1,132 @@
using Avalonia.Controls;
using Nonemm.Session;
namespace Nonemm.App.Windows;
/// Enter sends the message the contact has got to, which is how most operators
/// run N1MM. What goes out is decided by `Esm`; this keeps the state that
/// decision needs and sends what it asks for.
public sealed partial class EntryWindow
{
private bool callSent;
private bool exchangeSent;
/// The call as it was when it last went out, for the corrected call.
private string sentCall = "";
/// What the last Enter sent, so `=` can send it again.
private IReadOnlyList<int> lastKeys = [];
private bool IsEsmOn => session.Settings.EsmEnabled;
/// The keys the next Enter would send. The entry window highlights them, so
/// the operator can see what is about to happen.
private EsmAction NextEsmAction()
{
if (Logging is null)
{
return EsmAction.Nothing;
}
return Esm.Decide(new EsmSituation
{
IsRunning = Logging.IsRunning,
HasCall = Logging.Entry.Call.Trim().Length > 0,
IsDupe = Logging.Verdict()?.IsDupe == true,
HasExchange = Logging.Entry.IsComplete,
CallSent = callSent,
ExchangeSent = exchangeSent,
SendsCallOnce = session.Settings.EsmSendsCallOnce,
WorksDupes = session.Settings.EsmWorksDupes,
});
}
private async Task RunEsmAsync()
{
EsmAction action = NextEsmAction();
if (action.IsNothing)
{
Status("nothing to send — the station is already in the log");
return;
}
await SendKeysAsync(action.Keys);
if (action.Logs)
{
LogContact();
}
Refresh();
}
private async Task SendKeysAsync(IReadOnlyList<int> keys)
{
if (Logging is null)
{
return;
}
lastKeys = keys;
foreach (int key in keys)
{
await SendKeyAsync(key, CorrectedCall(key));
Remember(key);
}
}
/// N1MM's "send corrected call": while running, a call that has changed
/// since it went out is sent again in front of the message that ends the
/// contact, so the station hears the call we are logging.
private string CorrectedCall(int key)
{
if (Logging is null
|| key != Esm.EndQso
|| !Logging.IsRunning
|| !session.Settings.EsmSendsCorrectedCall)
{
return "";
}
string call = Logging.Entry.Call.Trim();
return call.Length > 0 && sentCall.Length > 0 && call != sentCall ? call + " " : "";
}
private void Remember(int key)
{
if (key is Esm.MyCall or Esm.HisCall)
{
callSent = true;
sentCall = Logging is null ? "" : Logging.Entry.Call.Trim();
}
if (key == Esm.Exchange)
{
exchangeSent = true;
}
}
/// The contact is over, or the boxes have been cleared: the next station
/// starts from nothing sent.
private void ResetEsm()
{
callSent = false;
exchangeSent = false;
sentCall = "";
lastKeys = [];
}
/// `=` sends whatever Enter last sent, without deciding again.
private void RepeatLastMessage()
{
if (lastKeys.Count == 0)
{
Status("nothing has been sent yet");
return;
}
_ = SendKeysAsync(lastKeys);
}
private void ShowEsmKeys()
{
EsmItem.IsChecked = IsEsmOn;
IReadOnlyList<int> keys = IsEsmOn ? NextEsmAction().Keys : [];
for (int at = 0; at < functionButtons.Count; at++)
{
functionButtons[at].Classes.Set("esm", keys.Contains(at));
}
}
}