Put the bands down the side of the entry window
N1MM has two columns of band buttons beside its entry window, CW in one and phone in the other, with the band the radio is on marked. This has the same. A button does not jump to the band edge every time: BandMemory keeps the frequency the operator was last on for each band and mode, so leaving 20 metres at 14032 and coming back lands on 14032. The first visit to a band starts where that part of it starts, which the band plan says — the bottom edge for CW, the CW-to-phone boundary for phone — and a band the plan says nothing about starts at its bottom edge. Phone picks the sideband the band calls for, so 40 metres is lower sideband and 20 is upper. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -173,6 +173,13 @@ in again while you are running, and `EsmSendsCorrectedCall` sends the call again
|
|||||||
in front of the last message when you have corrected it — copy `SM3AB`, fix it
|
in front of the last message when you have corrected it — copy `SM3AB`, fix it
|
||||||
to `SM3ABC`, and the key sends `SM3ABC TU DL1ABC`.
|
to `SM3ABC`, and the key sends `SM3ABC TU DL1ABC`.
|
||||||
|
|
||||||
|
### The band buttons
|
||||||
|
|
||||||
|
Down the left of the entry window are the bands, CW in one column and phone in
|
||||||
|
the other, as N1MM has them. A button puts the radio where you left that band
|
||||||
|
and mode, or at the start of that part of the band the first time. The button
|
||||||
|
for where the radio is now is marked.
|
||||||
|
|
||||||
### Where the other station is
|
### Where the other station is
|
||||||
|
|
||||||
Under the entry window is the line N1MM writes there: the beam heading, the
|
Under the entry window is the line N1MM writes there: the beam heading, the
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ public sealed class AppSession : IDisposable
|
|||||||
|
|
||||||
public Bandmap Bandmap { get; } = new();
|
public Bandmap Bandmap { get; } = new();
|
||||||
|
|
||||||
|
/// Where the operator was last on each band and mode, for the band buttons.
|
||||||
|
public BandMemory BandMemory { get; } = new();
|
||||||
|
|
||||||
/// The contest in progress: one log and one score, however many radios.
|
/// The contest in progress: one log and one score, however many radios.
|
||||||
public ContestSession? Logging { get; private set; }
|
public ContestSession? Logging { get; private set; }
|
||||||
|
|
||||||
|
|||||||
89
src/Nonemm.App/Windows/EntryWindow.Bands.cs
Normal file
89
src/Nonemm.App/Windows/EntryWindow.Bands.cs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Nonemm.Core;
|
||||||
|
|
||||||
|
namespace Nonemm.App.Windows;
|
||||||
|
|
||||||
|
/// The band buttons down the left of the window, CW in one column and phone in
|
||||||
|
/// the other, as N1MM has them. A button puts the radio where the operator left
|
||||||
|
/// that band, or at the start of that part of it.
|
||||||
|
public sealed partial class EntryWindow
|
||||||
|
{
|
||||||
|
private static readonly IReadOnlyList<Band> BandColumn =
|
||||||
|
[
|
||||||
|
Bands.Band160M, Bands.Band80M, Bands.Band40M, Bands.Band30M, Bands.Band20M,
|
||||||
|
Bands.Band17M, Bands.Band15M, Bands.Band12M, Bands.Band10M, Bands.Band6M,
|
||||||
|
];
|
||||||
|
|
||||||
|
private readonly List<(Band Band, ModeCategory Mode, Button Button)> bandButtons = [];
|
||||||
|
|
||||||
|
private void BuildBandButtons()
|
||||||
|
{
|
||||||
|
BandButtons.Children.Clear();
|
||||||
|
BandButtons.RowDefinitions.Clear();
|
||||||
|
bandButtons.Clear();
|
||||||
|
BandButtons.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
|
||||||
|
Heading("CW", 0);
|
||||||
|
Heading("PH", 1);
|
||||||
|
for (int at = 0; at < BandColumn.Count; at++)
|
||||||
|
{
|
||||||
|
BandButtons.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
|
||||||
|
Add(BandColumn[at], ModeCategory.Cw, at + 1, column: 0);
|
||||||
|
Add(BandColumn[at], ModeCategory.Phone, at + 1, column: 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Heading(string text, int column)
|
||||||
|
{
|
||||||
|
TextBlock heading = new()
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
FontSize = 10,
|
||||||
|
Opacity = 0.7,
|
||||||
|
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
|
Margin = new Avalonia.Thickness(0, 0, 2, 2),
|
||||||
|
};
|
||||||
|
Grid.SetColumn(heading, column);
|
||||||
|
BandButtons.Children.Add(heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Add(Band band, ModeCategory mode, int row, int column)
|
||||||
|
{
|
||||||
|
Button button = new() { Content = band.MegahertzLabel.ToString("0.###") };
|
||||||
|
button.Classes.Add("band");
|
||||||
|
button.Click += (_, _) => MoveToBand(band, mode);
|
||||||
|
Grid.SetRow(button, row);
|
||||||
|
Grid.SetColumn(button, column);
|
||||||
|
BandButtons.Children.Add(button);
|
||||||
|
bandButtons.Add((band, mode, button));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MoveToBand(Band band, ModeCategory mode)
|
||||||
|
{
|
||||||
|
if (Logging is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Frequency where = session.BandMemory.Where(band, mode, session.BandPlan);
|
||||||
|
Mode wanted = mode == ModeCategory.Phone ? Modes.ForSideband(where) : Modes.Cw;
|
||||||
|
Logging.Tune(where, wanted);
|
||||||
|
_ = session.Radio?.TuneAsync(where);
|
||||||
|
boxes[0].Focus();
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The button for the band and mode the radio is on is marked, and the
|
||||||
|
/// frequency is kept so the button goes back to it.
|
||||||
|
private void ShowBandButtons()
|
||||||
|
{
|
||||||
|
if (Logging is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
session.BandMemory.Remember(Logging.Frequency, Logging.Mode);
|
||||||
|
Band? here = Bands.ForFrequency(Logging.Frequency);
|
||||||
|
foreach ((Band band, ModeCategory mode, Button button) in bandButtons)
|
||||||
|
{
|
||||||
|
button.Classes.Set("here", band == here && mode == Logging.Mode.Category);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,17 @@
|
|||||||
<Setter Property="Padding" Value="6,2" />
|
<Setter Property="Padding" Value="6,2" />
|
||||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style Selector="Button.band">
|
||||||
|
<Setter Property="FontSize" Value="11" />
|
||||||
|
<Setter Property="Padding" Value="4,1" />
|
||||||
|
<Setter Property="Margin" Value="0,0,2,2" />
|
||||||
|
<Setter Property="MinWidth" Value="34" />
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.band.here">
|
||||||
|
<Setter Property="Background" Value="#2980B9" />
|
||||||
|
<Setter Property="Foreground" Value="White" />
|
||||||
|
</Style>
|
||||||
<Style Selector="Button.fkey">
|
<Style Selector="Button.fkey">
|
||||||
<Setter Property="FontSize" Value="11" />
|
<Setter Property="FontSize" Value="11" />
|
||||||
<Setter Property="Padding" Value="6,3" />
|
<Setter Property="Padding" Value="6,3" />
|
||||||
@@ -82,6 +93,11 @@
|
|||||||
<WrapPanel Name="FunctionKeys" />
|
<WrapPanel Name="FunctionKeys" />
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<Border DockPanel.Dock="Left" Padding="6,6,4,6" BorderThickness="0,0,1,0"
|
||||||
|
BorderBrush="#33808080">
|
||||||
|
<Grid Name="BandButtons" ColumnDefinitions="Auto,Auto" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
<StackPanel Margin="6">
|
<StackPanel Margin="6">
|
||||||
<Grid ColumnDefinitions="Auto,Auto,Auto,*,Auto" Margin="0,0,0,6">
|
<Grid ColumnDefinitions="Auto,Auto,Auto,*,Auto" Margin="0,0,0,6">
|
||||||
<TextBlock Name="FrequencyText" FontFamily="monospace" FontSize="20" Text="14025.00" />
|
<TextBlock Name="FrequencyText" FontFamily="monospace" FontSize="20" Text="14025.00" />
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public sealed partial class EntryWindow : Window
|
|||||||
this.radioNumber = radioNumber;
|
this.radioNumber = radioNumber;
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
BuildFunctionKeys();
|
BuildFunctionKeys();
|
||||||
|
BuildBandButtons();
|
||||||
session.Changed += (_, _) => Dispatcher.UIThread.Post(Refresh);
|
session.Changed += (_, _) => Dispatcher.UIThread.Post(Refresh);
|
||||||
session.ContestChanged += (_, _) => Dispatcher.UIThread.Post(() =>
|
session.ContestChanged += (_, _) => Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
@@ -396,6 +397,7 @@ public sealed partial class EntryWindow : Window
|
|||||||
private void Refresh()
|
private void Refresh()
|
||||||
{
|
{
|
||||||
ShowFunctionKeys();
|
ShowFunctionKeys();
|
||||||
|
ShowBandButtons();
|
||||||
if (Logging is null)
|
if (Logging is null)
|
||||||
{
|
{
|
||||||
VerdictText.Text = "";
|
VerdictText.Text = "";
|
||||||
|
|||||||
37
src/Nonemm.Session/BandMemory.cs
Normal file
37
src/Nonemm.Session/BandMemory.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using Nonemm.Core;
|
||||||
|
|
||||||
|
namespace Nonemm.Session;
|
||||||
|
|
||||||
|
/// Where the operator was last on each band and mode. The band buttons use it,
|
||||||
|
/// so moving to 40 metres and back puts the radio where it was rather than at
|
||||||
|
/// the band edge. N1MM remembers the same thing.
|
||||||
|
public sealed class BandMemory
|
||||||
|
{
|
||||||
|
private readonly Dictionary<(string Band, ModeCategory Mode), Frequency> places = [];
|
||||||
|
|
||||||
|
public void Remember(Frequency frequency, Mode mode)
|
||||||
|
{
|
||||||
|
if (Bands.ForFrequency(frequency) is { } band)
|
||||||
|
{
|
||||||
|
places[(band.Name, mode.Category)] = frequency;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where to go for that band and mode: where the operator left it, or the
|
||||||
|
/// start of that part of the band. A band the plan says nothing about
|
||||||
|
/// starts at its bottom edge.
|
||||||
|
public Frequency Where(Band band, ModeCategory mode, BandPlan plan)
|
||||||
|
{
|
||||||
|
if (places.TryGetValue((band.Name, mode), out Frequency remembered))
|
||||||
|
{
|
||||||
|
return remembered;
|
||||||
|
}
|
||||||
|
BandSegments? segments = plan.For(band);
|
||||||
|
return mode switch
|
||||||
|
{
|
||||||
|
ModeCategory.Phone when segments is { HasPhone: true } => segments.PhoneLow,
|
||||||
|
ModeCategory.Digital when segments is { HasDigital: true } => segments.DigitalLow,
|
||||||
|
_ => band.Low,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
57
tests/Nonemm.Session.Tests/BandMemoryTests.cs
Normal file
57
tests/Nonemm.Session.Tests/BandMemoryTests.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using Nonemm.Core;
|
||||||
|
|
||||||
|
namespace Nonemm.Session.Tests;
|
||||||
|
|
||||||
|
public class BandMemoryTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void AnUnvisitedBandStartsWhereThatPartOfItStarts()
|
||||||
|
{
|
||||||
|
BandMemory memory = new();
|
||||||
|
|
||||||
|
Assert.Equal(Bands.Band20M.Low, memory.Where(Bands.Band20M, ModeCategory.Cw, BandPlan.Default));
|
||||||
|
Assert.Equal(
|
||||||
|
Frequency.FromKilohertz(14_150),
|
||||||
|
memory.Where(Bands.Band20M, ModeCategory.Phone, BandPlan.Default));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TheBandComesBackWhereItWasLeft()
|
||||||
|
{
|
||||||
|
BandMemory memory = new();
|
||||||
|
memory.Remember(Frequency.FromKilohertz(14_032), Modes.Cw);
|
||||||
|
memory.Remember(Frequency.FromKilohertz(7_012), Modes.Cw);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
Frequency.FromKilohertz(14_032),
|
||||||
|
memory.Where(Bands.Band20M, ModeCategory.Cw, BandPlan.Default));
|
||||||
|
Assert.Equal(
|
||||||
|
Frequency.FromKilohertz(7_012),
|
||||||
|
memory.Where(Bands.Band40M, ModeCategory.Cw, BandPlan.Default));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EachModeOnABandIsRememberedOnItsOwn()
|
||||||
|
{
|
||||||
|
BandMemory memory = new();
|
||||||
|
memory.Remember(Frequency.FromKilohertz(14_032), Modes.Cw);
|
||||||
|
memory.Remember(Frequency.FromKilohertz(14_240), Modes.Usb);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
Frequency.FromKilohertz(14_032),
|
||||||
|
memory.Where(Bands.Band20M, ModeCategory.Cw, BandPlan.Default));
|
||||||
|
Assert.Equal(
|
||||||
|
Frequency.FromKilohertz(14_240),
|
||||||
|
memory.Where(Bands.Band20M, ModeCategory.Phone, BandPlan.Default));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ABandWithNoPlanStartsAtItsBottomEdge()
|
||||||
|
{
|
||||||
|
BandMemory memory = new();
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
Bands.Band60M.Low,
|
||||||
|
memory.Where(Bands.Band60M, ModeCategory.Phone, BandPlan.Default));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user