using Avalonia.Threading; using Nonemm.Core; using Nonemm.Keying; using Nonemm.Session; using Nonemm.Spotting; namespace Nonemm.App.Windows; /// Running a function key message: the action macros in it, the text that goes /// on the air, and the actions N1MM's `{END}` holds back until the message has /// gone out. public sealed partial class EntryWindow { /// A keyer that says nothing after this long is taken as finished, so the /// actions after `{END}` still run. private static readonly TimeSpan SendingPatience = TimeSpan.FromMinutes(2); private void SendMessage(int index) => _ = SendKeyAsync(index); /// Sends one function key's message. The task finishes when the text has /// been handed to the keyer, not when it has gone out on the air, so a /// second key can be queued behind it. `prefix` carries the corrected call /// that ESM puts in front of the message that ends a contact. private async Task SendKeyAsync(int index, string prefix = "") { if (Logging is null) { return; } string template = Messages.For( Logging.Mode.Category, session.Settings.CwMessages, session.Settings.PhoneMessages)[index]; if (template.Length == 0) { return; } MessagePlan plan = MessagePlan.Read(template, Logging, session.Other(Logging)); foreach (MessageAction action in plan.Before) { Run(action); } Refresh(); if (plan.Text.Length == 0) { // a key that only acts: {WIPE}, {RUN}, {TELNET sh/dx} return; } if (session.Keyer is null) { Status("no keyer — Config ▸ Keyer"); return; } string text = prefix + plan.Text; // the box has to point at this radio before the key does await session.PointTransmitAtAsync(radioNumber); Status($"sending {text}"); await SendThenAsync(session.Keyer, text, plan.After); } /// Hands the text to the keyer, and leaves what follows `{END}` to run on /// its own once the keyer says the message has gone out. private async Task SendThenAsync(MessageSender keyer, string text, IReadOnlyList after) { try { await keyer.SendAsync(text); } catch (InvalidOperationException e) { Status(e.Message); return; } if (after.Count > 0) { _ = RunWhenSentAsync(keyer, after); } } private async Task RunWhenSentAsync(MessageSender keyer, IReadOnlyList after) { await WhenSentAsync(keyer); Dispatcher.UIThread.Post(() => { foreach (MessageAction action in after) { Run(action); } Refresh(); }); } /// Waits for the keyer to say the message has gone out. A keyer that does /// not report completion is taken at its word as soon as the text is sent, /// because the alternative is never running what follows `{END}`. private static async Task WhenSentAsync(MessageSender keyer) { if (!keyer.ReportsCompletion) { return; } TaskCompletionSource sent = new(); void Finished(object? sender, EventArgs e) => sent.TrySetResult(); keyer.Finished += Finished; try { await sent.Task.WaitAsync(SendingPatience); } catch (TimeoutException) { } finally { keyer.Finished -= Finished; } } private void Run(MessageAction action) { if (Logging is null) { return; } switch (action.Command) { case MessageCommand.Wipe: Logging.Wipe(); SyncBoxes(); boxes[0].Focus(); break; case MessageCommand.Log: LogContact(); break; case MessageCommand.Run: Logging.IsRunning = true; break; case MessageCommand.SearchAndPounce: Logging.IsRunning = false; break; case MessageCommand.Space: MoveFocus(forward: true); break; case MessageCommand.SpotMe: SpotMe(); break; case MessageCommand.ExchangeSent: exchangeSent = true; break; // the manual's table has the two page macros the other way round; // the keys they are named after move the frequency this way case MessageCommand.FrequencyUp: Step(session.Settings.FrequencyStepHertz); break; case MessageCommand.FrequencyDown: Step(-session.Settings.FrequencyStepHertz); break; case MessageCommand.PageUp: Step(session.Settings.PageStepHertz); break; case MessageCommand.PageDown: Step(-session.Settings.PageStepHertz); break; case MessageCommand.StopSending: _ = session.Keyer?.AbortAsync(); break; case MessageCommand.Telnet: SendToCluster(action.Argument); break; case MessageCommand.JumpToOtherRadio: session.SwapRadio(); break; case MessageCommand.JumpToOtherRadioWithTransmit: session.SwapRadio(); _ = session.PointTransmitAtAsync(session.ActiveRadioNumber); break; case MessageCommand.SendOnOtherRadio: SendOnOtherRadio(action.Argument); break; case MessageCommand.Otrsp: _ = session.Box?.SendCommandAsync(action.Argument); break; } } private void Step(int hertz) { if (Logging is null) { return; } Frequency moved = Frequency.FromHertz(Logging.Frequency.Hertz + hertz); Logging.Tune(moved); _ = session.Radio?.TuneAsync(moved); Refresh(); } /// N1MM only self-spots while running, and so does this: a station that is /// not calling CQ has nothing to tell the cluster. private void SpotMe() { if (Logging is null || session.Cluster is not { IsConnected: true } cluster) { Status("not connected to a cluster node"); return; } if (!Logging.IsRunning) { Status("self-spotting is for running only"); return; } Callsign me = Callsign.Parse(Logging.Me.Callsign); string comment = MessageExpander.Expand( session.Settings.SpotComment, Logging, session.Other(Logging)); _ = cluster.SendSpotAsync(Logging.Frequency, me, comment); session.Bandmap.Add(new Spot(me, Logging.Frequency, DateTime.UtcNow, SpotSource.Operator)); Status($"spotted {me.Text} on {Logging.Frequency.Kilohertz:0.0}"); } private void SendToCluster(string command) { if (session.Cluster is not { } cluster) { Status("not connected to a cluster node"); return; } _ = cluster.SendAsync(command); Status($"cluster: {command}"); } /// `{CTRLFn}` sends the other radio's message n while the keyboard stays /// here, which is how an SO2R station CQs on the other radio. private void SendOnOtherRadio(string key) { if (Logging is null || session.Other(Logging) is not { } other || session.Keyer is null || !int.TryParse(key, out int number)) { return; } string template = Messages.For( other.Mode.Category, session.Settings.CwMessages, session.Settings.PhoneMessages)[number - 1]; if (template.Length == 0) { return; } string text = MessageExpander.Expand(template, other, Logging); _ = SendOnOtherRadioAsync(other.RadioNumber, text); Status($"radio {other.RadioNumber}: {text}"); } private async Task SendOnOtherRadioAsync(int radio, string text) { await session.PointTransmitAtAsync(radio); try { await session.Keyer!.SendAsync(text); } catch (InvalidOperationException e) { Status(e.Message); } } }