Add CW keying, function key messages and the documentation

The function keys send through cwdaemon or a WinKeyer, with N1MM's message
macros. Escape stops sending.

Settings are read with the reflection serializer rather than a generated one:
the generated one hands back null for every property the file leaves out
instead of the value the property is declared with, which crashed the program
the first time a new setting was added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik
2026-08-27 11:23:30 +00:00
parent d96cbe146b
commit f5aa77ece3
25 changed files with 1002 additions and 26 deletions

View File

@@ -254,6 +254,27 @@ public sealed partial class EntryWindow
}
}
private async void OnKeyerSettings(object? sender, RoutedEventArgs e)
{
KeyerDialog dialog = new(session.Settings);
Settings? updated = await dialog.ShowDialog<Settings?>(this);
if (updated is null)
{
return;
}
session.Save(updated);
try
{
session.ApplyKeyerSettings();
Status(updated.KeyerKind == "none" ? "no keyer" : $"keyer: {updated.KeyerKind}");
}
catch (Exception error) when (error is InvalidOperationException or IOException or UnauthorizedAccessException)
{
Status($"could not open the keyer: {error.Message}");
}
BuildFunctionKeys();
}
private async void OnDownloadCountryFile(object? sender, RoutedEventArgs e) =>
await Download(
downloader => downloader.DownloadCountryFileAsync(session.Paths.CountryFile),

View File

@@ -51,6 +51,7 @@
<MenuItem Header="_Radio…" Click="OnRadioSettings" />
<MenuItem Header="_Cluster…" Click="OnClusterSettings" />
<MenuItem Header="_Network…" Click="OnNetworkSettings" />
<MenuItem Header="_Keyer and messages…" Click="OnKeyerSettings" />
<Separator />
<MenuItem Header="Download Country _File" Click="OnDownloadCountryFile" />
<MenuItem Header="Download Check _Partial File" Click="OnDownloadCallDatabase" />

View File

@@ -55,9 +55,47 @@ public sealed partial class EntryWindow : Window
{
Status($"could not reopen the last log: {e.Message}");
}
StartConnections();
BuildEntryBoxes();
}
/// Brings up whatever the operator had connected last time. Each is
/// reported on its own so one that fails does not stop the others.
private void StartConnections()
{
foreach ((string what, Action start) in Connections())
{
try
{
start();
}
catch (Exception e) when (e is InvalidOperationException or IOException or UnauthorizedAccessException)
{
Status($"could not start the {what}: {e.Message}");
}
}
}
private IEnumerable<(string What, Action Start)> Connections()
{
if (session.Settings.RadioEnabled)
{
yield return ("radio", session.ConnectRadio);
}
if (session.Settings.ClusterEnabled)
{
yield return ("cluster", session.ConnectCluster);
}
if (session.Settings.NetworkEnabled)
{
yield return ("station network", session.ApplyNetworkSettings);
}
if (session.Settings.KeyerKind != "none")
{
yield return ("keyer", session.ApplyKeyerSettings);
}
}
private void BuildEntryBoxes()
{
EntryGrid.Children.Clear();
@@ -132,6 +170,7 @@ public sealed partial class EntryWindow : Window
break;
case Key.Escape:
e.Handled = true;
_ = session.Keyer?.AbortAsync();
Logging.Wipe();
SyncBoxes();
boxes[0].Focus();
@@ -144,6 +183,10 @@ public sealed partial class EntryWindow : Window
e.Handled = true;
ToggleRun();
break;
case >= Key.F1 and <= Key.F12:
e.Handled = true;
RunFunctionKey(e.Key - Key.F1);
break;
}
}
@@ -282,11 +325,85 @@ public sealed partial class EntryWindow : Window
private void BuildFunctionKeys()
{
FunctionKeys.Children.Clear();
foreach ((string key, string label, _) in Messages.Defaults)
for (int at = 0; at < Messages.Keys.Count; at++)
{
int index = at;
(string key, string label) = Messages.Keys[at];
Button button = new() { Content = $"{key} {label}" };
button.Classes.Add("fkey");
button.Click += (_, _) => RunFunctionKey(index);
FunctionKeys.Children.Add(button);
}
}
/// F11 spots the station being worked, F12 wipes the entry, and the rest
/// send their message.
private void RunFunctionKey(int index)
{
if (Logging is null)
{
return;
}
switch (index)
{
case 10:
SpotCurrentCall();
return;
case 11:
Logging.Wipe();
SyncBoxes();
boxes[0].Focus();
return;
default:
SendMessage(index);
return;
}
}
private void SendMessage(int index)
{
if (Logging is null || session.Keyer is null)
{
Status("no keyer — Config ▸ Keyer");
return;
}
string template = Messages.For(
Logging.Mode.Category,
session.Settings.CwMessages,
session.Settings.PhoneMessages)[index];
if (template.Length == 0)
{
return;
}
string text = MessageExpander.Expand(template, Logging);
Status($"sending {text}");
_ = SendAsync(text);
}
private async Task SendAsync(string text)
{
try
{
await session.Keyer!.SendAsync(text);
}
catch (InvalidOperationException e)
{
Status(e.Message);
}
}
private void SpotCurrentCall()
{
if (Logging is null || Logging.Entry.Call.Trim().Length == 0)
{
return;
}
session.Bandmap.Add(new Nonemm.Spotting.Spot(
Core.Callsign.Parse(Logging.Entry.Call.Trim()),
Logging.Frequency,
DateTime.UtcNow,
Nonemm.Spotting.SpotSource.Operator,
Logging.Me.Callsign));
Status($"{Logging.Entry.Call.Trim()} put on the bandmap");
}
}