From d084789ed24fc0bdc42d5eb39ba6ff8d247098ce Mon Sep 17 00:00:00 2001 From: ericek111 Date: Thu, 27 Aug 2026 23:01:56 +0000 Subject: [PATCH] Paint the band the way N1MM paints it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Behind the bandmap scale, the CW part of the band is one shade and the phone part another, with a third for a digital block. That is what N1MM's graphical bandmap does: ResizeBandPanels puts a CW, a Digi and an SSB panel behind the scale, sized from its CWBands, DigiBands and SSBBands tables. Anything past a band edge is shaded red, which is the only place the edges themselves show, and it only appears now that the slice can reach past an edge. The defaults are N1MM's: CW ends at 1840, 3600, 7125, 14150 and so on, and the digital pair is empty until somebody fills it in, exactly as N1MM ships. They are one worldwide set rather than one per ITU region, which is also N1MM's answer — the reason recorded for leaving this out, that the segments differ by region, was wrong about what N1MM holds. 60M and everything above 2M get no entry, because N1MM's own numbers there contradict themselves: its 1.25M CW top is below its phone start, and its 70CM CW top is above the band. Config ▸ Sub bands edits them, one row per band in kilohertz, with a Defaults button. Only the rows that differ are written to the settings file, so a plan nobody has touched leaves nothing behind. BandPlan is an object rather than a static table, so the stored changes layer over the defaults and the bandmap picks up a save without a restart. The numbers are a drawing aid, not a licence. Nothing stops a contact being logged either side of a boundary. Co-Authored-By: Claude Opus 5 --- README.md | 17 ++- docs/unfinished.md | 10 +- src/Nonemm.App/AppSession.cs | 5 + src/Nonemm.App/Configuration/Settings.cs | 50 +++++++ src/Nonemm.App/Dialogs/SubBandsDialog.axaml | 20 +++ .../Dialogs/SubBandsDialog.axaml.cs | 125 ++++++++++++++++++ src/Nonemm.App/Windows/BandmapView.cs | 45 +++++++ src/Nonemm.App/Windows/BandmapWindow.axaml.cs | 11 +- src/Nonemm.App/Windows/EntryWindow.Menu.cs | 18 +++ src/Nonemm.App/Windows/EntryWindow.axaml | 1 + src/Nonemm.Core/BandPlan.cs | 75 +++++++++++ tests/Nonemm.Core.Tests/BandPlanTests.cs | 61 +++++++++ 12 files changed, 431 insertions(+), 7 deletions(-) create mode 100644 src/Nonemm.App/Dialogs/SubBandsDialog.axaml create mode 100644 src/Nonemm.App/Dialogs/SubBandsDialog.axaml.cs create mode 100644 src/Nonemm.Core/BandPlan.cs create mode 100644 tests/Nonemm.Core.Tests/BandPlanTests.cs diff --git a/README.md b/README.md index 844941f..1911e7c 100644 --- a/README.md +++ b/README.md @@ -229,8 +229,21 @@ The bars on the scale are as wide as the mode passes: green where you are listening, red where the radio transmits when working split, orange for the other radio of a two-radio station. -Band-plan colouring of the scale is not there. The segments differ by ITU region -and the program has no band-plan table, so it would be guesswork. +Behind the scale the CW part of the band is one shade and the phone part +another, with a third shade for a digital block where one is set, and the CW +boundary named in the status line. Anything past a band edge is shaded red, +which is where the edges show. N1MM paints the same blocks from its `CWBands`, +`DigiBands` and `SSBBands` tables. + +**Config → Sub bands** edits them, one row per band, in kilohertz, with a +Defaults button. The digital pair starts empty, as N1MM's does, so nothing is +painted for it until it is filled in. Only the rows that differ from the +defaults are written to the settings file. + +The defaults are one worldwide set, as N1MM's are, not one per ITU region. They +are a drawing aid: a contact logs either side of a boundary just the same. +Bands where N1MM's own numbers contradict themselves — 60M, and everything +above 2M — get no shading rather than a guess. ### Band edges diff --git a/docs/unfinished.md b/docs/unfinished.md index 25a6b73..05612b7 100644 --- a/docs/unfinished.md +++ b/docs/unfinished.md @@ -45,10 +45,6 @@ data. The file is read without them. the contest rules score it, but there is no digital window: no decoding, no transmitting, no interface to fldigi, MMTTY or similar. -**Band-plan colouring of the bandmap scale.** N1MM colours the scale by CW, -digital and phone segment. The segments differ by ITU region and there is no -band-plan table here, so it is left out rather than guessed. - **Contest coverage.** Eight contests are built in, plus whatever `.udc` files are in the user-defined folder. N1MM ships well over a hundred. Opening a log from a contest not in the registry fails with the contest name, which is the @@ -64,6 +60,12 @@ thread, and `AppSession.TakeFromNetwork` reopens the whole contest to do it. That is correct but heavy, and it discards what is typed in an entry window on another radio. +**The band plan covers 160M to 2M only.** 60M and everything above 2M get no +CW, digital or phone shading on the bandmap, because N1MM's own numbers for +those bands contradict themselves — its CW top for 1.25M is below its phone +start, and its 70CM CW top is above the band. Config ▸ Sub bands cannot add a +band that has no default. + **Available mults and Qs is only as good as the bandmap.** It lists what is spotted, so a multiplier nobody has spotted is not there. N1MM's window also shows the mults a contest has and nobody has worked, which needs a list of every diff --git a/src/Nonemm.App/AppSession.cs b/src/Nonemm.App/AppSession.cs index da05c1f..7872761 100644 --- a/src/Nonemm.App/AppSession.cs +++ b/src/Nonemm.App/AppSession.cs @@ -36,6 +36,7 @@ public sealed class AppSession : IDisposable History = LoadCallHistory(settings.CallHistoryFile); Registry = ContestRegistry.FromFolder(paths.UserDefinedContests, out IReadOnlyList problems); UserDefinedContestProblems = problems; + BandPlan = settings.ToBandPlan(); } public UserPaths Paths { get; } @@ -44,6 +45,9 @@ public sealed class AppSession : IDisposable public ContestRegistry Registry { get; private set; } + /// Where the bandmap paints the CW, digital and phone parts of a band. + public BandPlan BandPlan { get; private set; } + public IReadOnlyList UserDefinedContestProblems { get; } public CountryFile? Countries { get; private set; } @@ -101,6 +105,7 @@ public sealed class AppSession : IDisposable public void Save(Settings settings) { Settings = settings; + BandPlan = settings.ToBandPlan(); settings.Save(Paths.SettingsFile); Changed?.Invoke(this, EventArgs.Empty); } diff --git a/src/Nonemm.App/Configuration/Settings.cs b/src/Nonemm.App/Configuration/Settings.cs index 1cb6620..b567133 100644 --- a/src/Nonemm.App/Configuration/Settings.cs +++ b/src/Nonemm.App/Configuration/Settings.cs @@ -62,6 +62,10 @@ public sealed record Settings public int KeyerSpeed { get; init; } = 28; + /// Sub-band boundaries the operator has changed. Empty means the defaults + /// in `BandPlan.Default`; an entry replaces one band's boundaries. + public IReadOnlyList SubBands { get; init; } = []; + /// The twelve function key messages, keyed F1 to F12. public IReadOnlyList CwMessages { get; init; } = []; @@ -91,6 +95,20 @@ public sealed record Settings } } + /// The defaults with the operator's changes applied over them. + public BandPlan ToBandPlan() + { + BandPlan plan = BandPlan.Default; + foreach (StoredSubBand stored in SubBands) + { + if (stored.ToSegments() is { } segments) + { + plan = plan.With(segments); + } + } + return plan; + } + public void Save(string path) => File.WriteAllText(path, JsonSerializer.Serialize(this, Json)); @@ -112,6 +130,38 @@ public sealed record Settings }; } +/// One band's sub-band boundaries as they are stored. Kilohertz, because that +/// is what the operator types into the editor. +public sealed record StoredSubBand +{ + public string Band { get; init; } = ""; + + public double CwHigh { get; init; } + + public double DigitalLow { get; init; } + + public double DigitalHigh { get; init; } + + /// Null when the band name is not one we know, or the CW boundary is + /// missing: an entry that cannot be read leaves the default in place. + public BandSegments? ToSegments() => + Bands.Named(Band) is { } band && CwHigh > 0 + ? new BandSegments( + band, + Frequency.FromKilohertz(CwHigh), + Frequency.FromKilohertz(DigitalLow), + Frequency.FromKilohertz(DigitalHigh)) + : null; + + public static StoredSubBand From(BandSegments segments) => new() + { + Band = segments.Band.Name, + CwHigh = segments.CwHigh.Kilohertz, + DigitalLow = segments.DigitalLow.Kilohertz, + DigitalHigh = segments.DigitalHigh.Kilohertz, + }; +} + /// One radio's `rigctld`. public sealed record StoredRadio { diff --git a/src/Nonemm.App/Dialogs/SubBandsDialog.axaml b/src/Nonemm.App/Dialogs/SubBandsDialog.axaml new file mode 100644 index 0000000..2063a5d --- /dev/null +++ b/src/Nonemm.App/Dialogs/SubBandsDialog.axaml @@ -0,0 +1,20 @@ + + + + +