Files
Nonemm/src/Nonemm.Session/Esm.cs
T
erikbrockoandClaude Opus 5 0c440ed865 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>
2026-08-28 07:59:11 +00:00

110 lines
3.4 KiB
C#

namespace Nonemm.Session;
/// What the entry window looks like when Enter is pressed, which is all ESM
/// needs to know to decide what to send.
public sealed record EsmSituation
{
public bool IsRunning { get; init; }
public bool HasCall { get; init; }
/// The station has been worked before on this band and mode.
public bool IsDupe { get; init; }
/// Every exchange box holds something the contest accepts.
public bool HasExchange { get; init; }
/// Our call has gone out for this station: F4 while searching, F5 and the
/// exchange while running.
public bool CallSent { get; init; }
public bool ExchangeSent { get; init; }
/// N1MM's "big gun" switch: while searching, send the call once and then be
/// ready to copy the exchange, rather than calling again.
public bool SendsCallOnce { get; init; }
/// N1MM's "work dupes when running", which it recommends: a station that
/// calls in again is worked rather than told it is in the log.
public bool WorksDupes { get; init; }
}
/// The function keys Enter sends, in order, and whether the contact is logged
/// afterwards. No keys and no logging means Enter does nothing, which is what
/// happens when a dupe calls in while searching.
public sealed record EsmAction(IReadOnlyList<int> Keys, bool Logs)
{
public static readonly EsmAction Nothing = new([], false);
public bool IsNothing => Keys.Count == 0 && !Logs;
}
/// Enter sends the message the contact has got to. The table is N1MM's, from
/// its function-key documentation: the state of the callsign and exchange
/// boxes, and whether the station is running or searching, decide what goes
/// out.
public static class Esm
{
/// Which function key is which. N1MM's layout, and the one the entry
/// window's buttons are labelled with.
public const int CallCq = 0;
public const int Exchange = 1;
public const int EndQso = 2;
public const int MyCall = 3;
public const int HisCall = 4;
public const int WorkedBefore = 5;
public const int Again = 7;
public static EsmAction Decide(EsmSituation now) =>
now.IsRunning ? Running(now) : Searching(now);
private static EsmAction Running(EsmSituation now)
{
if (!now.HasCall)
{
return new EsmAction([CallCq], false);
}
if (now.HasExchange)
{
return now.ExchangeSent
? new EsmAction([EndQso], true)
: new EsmAction([HisCall, Exchange], false);
}
if (now.IsDupe && !now.WorksDupes)
{
return new EsmAction([WorkedBefore], false);
}
return now.CallSent
? new EsmAction([Again], false)
: new EsmAction([HisCall, Exchange], false);
}
private static EsmAction Searching(EsmSituation now)
{
if (!now.HasCall)
{
return new EsmAction([MyCall], false);
}
if (now.HasExchange)
{
return now.ExchangeSent
? new EsmAction([], true)
: new EsmAction([Exchange], true);
}
if (now.IsDupe)
{
// a station already in the log is not called again
return EsmAction.Nothing;
}
return now.CallSent && now.SendsCallOnce
? new EsmAction([Again], false)
: new EsmAction([MyCall], false);
}
}