Add the session, radio control, bandmap and cluster

Nonemm.Session holds what the operator is typing, what the log says about it and
what happens on Enter, with no UI toolkit behind it. Nonemm.Rig talks to
hamlib's rigctld and reconnects on its own. Nonemm.Spotting reads DX cluster
lines into a bandmap that drops spots after an hour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik
2026-08-27 10:39:53 +00:00
co-authored by Claude Opus 5
parent ef631f1dc4
commit ffeeb2cdc1
25 changed files with 1416 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
using Nonemm.Contests;
namespace Nonemm.Session;
/// What the operator has typed: the callsign, a value per exchange box, and
/// which box the cursor is in. The callsign box is field 0.
public sealed class EntryFields
{
private readonly List<string> values;
public EntryFields(IReadOnlyList<ExchangeField> exchange)
{
Exchange = exchange;
values = [.. Enumerable.Repeat("", exchange.Count)];
}
public IReadOnlyList<ExchangeField> Exchange { get; }
public string Call { get; set; } = "";
/// 0 is the callsign box; 1 and up are the exchange boxes in order.
public int Focus { get; private set; }
public int FieldCount => Exchange.Count + 1;
public string this[int field]
{
get => field == 0 ? Call : values[field - 1];
set
{
if (field == 0)
{
Call = value;
}
else
{
values[field - 1] = value;
}
}
}
public string ValueOf(ExchangeSlot slot)
{
for (int at = 0; at < Exchange.Count; at++)
{
if (Exchange[at].Slot == slot)
{
return values[at];
}
}
return "";
}
public void Set(ExchangeSlot slot, string value)
{
for (int at = 0; at < Exchange.Count; at++)
{
if (Exchange[at].Slot == slot)
{
values[at] = value;
}
}
}
public void FocusOn(int field) => Focus = Math.Clamp(field, 0, FieldCount - 1);
/// What space does: move to the next box, and round to the callsign again
/// from the last one.
public void Advance() => Focus = (Focus + 1) % FieldCount;
public void Retreat() => Focus = (Focus + FieldCount - 1) % FieldCount;
/// True when everything the contest asks for has been typed.
public bool IsComplete =>
Call.Trim().Length > 0 &&
Exchange.Where((f, at) => f.IsRequired && values[at].Trim().Length == 0).Any() == false;
public void Clear()
{
Call = "";
for (int at = 0; at < values.Count; at++)
{
values[at] = "";
}
Focus = 0;
}
}