Commit Graph
55 Commits
Author SHA1 Message Date
erikbrockoandClaude Sonnet 5.5 a189f63732 Show local spots in bold and name the computer that made them
A spot an operator put on a bandmap, here or on another computer, is
drawn in bold. Its hover text names the computer instead of the
spotter's callsign. A spot from another computer carries the name from
the ADDSPOT message.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 11:51:13 +00:00
erikbrockoandClaude Sonnet 5.5 f784a73115 Spot a call locally and wipe when the radio moves far on a QSY
With a call in the entry window, a move of more than 2 kHz on the same
band puts the call on the bandmap where it was heard and wipes the boxes,
as N1MM does. The spot goes to the other stations as ADDSPOT and to the
Spots broadcast, never to the cluster. ADDSPOT from other stations is
added to the bandmap.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 05:50:20 +00:00
erikbrockoandClaude Sonnet 5.5 072c04883e Mirror a radio from another computer's broadcast data
Listen for Radio broadcast data from another Nonemm or N1MM computer and
record its frequency, mode and split as this computer's own radio. No
radio is controlled. The settings are in the Configure Radios dialog.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 05:16:27 +00:00
erikbrockoandClaude Sonnet 5.5 65178b2d89 Add the Broadcast Data dialog and send N1MM's score breakdown
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 03:42:19 +00:00
erikbrockoandClaude Sonnet 5.5 ec12fb1d0f Start the station link on launch and beacon on every interface
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 03:22:34 +00:00
erikbrockoandClaude Opus 5 b5043506ae Fill in the macros from N1MM's function key page
Going down N1MM's published list, the ones a contest operator would miss:

| | |
|---|---|
| `{LASTEXCH}` | what the last station sent, built from the boxes the contest asks for |
| `{ROVERQTH}` | where a rover is now |
| `{TIME}`, `{DATE}`, `{TIMEGMT}`, `{DATEGMT}` | the clock and the calendar |
| `{ESC}`, `{CTRL-A}` to `{CTRL-Z}` | the control characters a TNC reads as commands |
| `{CTRLHOME}` | the cursor back in the callsign box |
| `{F1}` to `{F12}` | another of this radio's keys, where `{CTRLF1}` is the other radio's |
| `{CQ}` | makes any key a CQ key: it starts running and carries alternating CQ on |
| `{DIGQTCS}`, `{DIGQTCR}` | opens the WAE QTC window the way asked for |
| `{CLEARRIT}` | the receive offset back to zero, which is rigctld's `J 0` |
| `{FORCELOG}` | logs what is in the boxes, holes and all |

`{FORCELOG}` is the one that gives something up. `LogContact` refuses an entry
that is short or holds a value the contest will not take, so that a caller who
has not checked cannot log nonsense; `LogContactAnyway` is the way past it,
because a contact in the log with a hole in it can be filled in afterwards and
one that was never logged cannot. It still needs a callsign, and the status
line says the row will need fixing before the log goes in.

A key that names itself would send for ever, so a key sending another key
ignores the macro a second time down.

What is still passed over is written down in the README and unfinished.md.
Advanced voicing is the one that matters: N1MM says a callsign or a serial by
playing a recording per letter out of `LettersFiles`, which is why `!` and `#`
say nothing on phone here and why F5 is empty in N1MM's own SSB file too. The
three packet BBS date formats are passed over because there is nothing here to
check a guess at them against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-11 08:24:34 +00:00
erikbrockoandClaude Opus 5 31ad6fb192 Record a message with Ctrl+Shift+F, and open the editor with Alt+K
N1MM records a phone message from the entry window: Ctrl+Shift+F1 to
Ctrl+Shift+F12 records into the file that key plays, and the same pair again
stops and saves it. Escape and sending anything stop one too — there is one
microphone, and it cannot be recorded and transmitted at the same time. The
status line says what is being recorded and what was saved, which is where
N1MM says it.

The recording being made is the session's rather than the window's, for the
same reason: the voice keyer window and these keys record into the same
microphone, and now each can see what the other started.

Alt+K opens the function key editor for the mode the radio is on, which is
N1MM's shortcut for what right-clicking a key button already did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-11 08:24:21 +00:00
erikbrockoandClaude Opus 5 082fa75a40 Play a recording on phone, and record it in a window
A phone function key names a wav file instead of words, which is what N1MM's
own SSB file holds: `F1 CQ,{WAVDIR}\Cq.wav`. `{WAVDIR}` is the folder named
after whoever is at the microphone, a name is taken relative to `Wav`, and
commas play several files in a row. An SSB `.mc` file written for N1MM plays
here.

Window ▸ Voice Keyer lists every recording the messages name, says which of
them are there for the operator picked at the top, plays one, and records over
one. The recording is written beside the wanted file and moved over it at the
end, so a recorder that will not start takes nothing away.

`CommandSoundCard` runs `aplay` and `arecord`, and both commands are settings:
the audio path belongs to whatever the station runs, and a library would pick
one of them for the operator. A recorder is stopped with an interrupt so it
closes its wav file, and killed only if it ignores that. PTT is cwdaemon's
`<ESC>a` for as long as the recording plays, off for a station on VOX.

Two things follow. The QTC window sends on SSB: N1MM's four recordings on the
Ready, Agn, Cfm and Close buttons, with the lines still read out by the
operator. And alternating CQ duels on phone, because the voice keyer says when
a recording has finished — which it had to, since the CQ message is picked by
the radio's mode and would otherwise have been keyed as Morse.

Nothing here has been played on a radio: this machine has no sound card, so
what is tested is the program being run, the file filled in, the exit code and
the interrupt, with shell commands standing in for the two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-11 07:51:58 +00:00
erikbrockoandClaude Opus 5 b5d9f1e21c Name the All Asian multiplier after what it counts
The score window and the stored Contest.Multiplier1Name row both said
"Prefixes" for an Asian entrant, which counts DXCC entities.

Contest.MultiplierNames is now MultiplierNamesFor(StationInfo me), the
same shape as ExchangeFieldsFor. Every other contest returns the same
list it did before; All Asian returns "Countries" from Asia and
"Prefixes" from everywhere else. ContestDefinitions.For takes the
station as well, because that name is written to the log for N1MM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 20:47:51 +00:00
erikbrockoandClaude Opus 5 211298e04c Let the walk and the completeness check read one rule
A box the walk between the entry boxes steps over cannot be typed in, so it
must not hold the contact back either. `EntryFields.IsComplete` counted every
box marked required, while `MoveFocus` stepped over the ones the contest does
not ask this station for, so a contest with a box for some stations only
could lock the operator out of his own log.

The seven contests the scoring audit named do not do that any more —
`bae40bb` gave each of them the boxes N1MM shows, and fifteen reproductions
across all seven and both station types log cleanly — but the rule they broke
was still there to break again. Completeness now takes the same
applicability rule the cursor uses, and `OperatingPosition` answers it in one
place for the entry window, the ESM window and logging alike.

The audit is marked up for the three findings that are already fixed and for
item 14, which turns out to be what N1MM does rather than a fault.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 20:33:30 +00:00
erikbrockoandClaude Opus 5 0d9b4a713e Export WAE QTC traffic to ADIF, and read it back
ADIF has no record for QTC traffic, which is why N1MM leaves it out and why
a WAE log cannot be moved between databases as ADIF. The plain export now
writes the contacts alone, as N1MM does: it used to write the QTC rows as
contacts, with the time of the traffic in RST_SENT and the reported callsign
in RST_RCVD.

File > Export > Export ADIF with QTC traffic writes the whole log instead.
A QTC row goes out with the station, time, band and mode, and the traffic in
application-defined fields — APP_NONEMM_QTC and friends, the prefix the ADIF
specification keeps for a program's own data — with the line in the comment
as well so the file can be read without this program. The report columns are
left off that record, because on a QTC row they hold the time and callsign
of the contact being reported. Reading such a file back rebuilds the rows,
so a WAE log survives the round trip whole. The item is only in the menu for
a contest with traffic.

Another program reading that file sees each QTC row as an ordinary contact
with the station the traffic was exchanged with and ignores the extra
fields. There is no way around that in ADIF, which is why the plain export
is still the one to hand over.

Contacts now also carry their row kind in APP_NONEMM_EXCHANGE1. Without it
a WAE log read back holds contacts with no kind at all, which N1MM's own
queries do not find.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 19:31:04 +00:00
erikbrockoandClaude Opus 5 c686a86dcc Log the contact when Ctrl+Z opens the QTC window, ESM or not
Ctrl+Z logged the contact by calling the Enter handler. With ESM switched
on, Enter sends the next message of the contact instead of logging it, so
the QTC window opened over an unlogged contact and the traffic went into the
log in front of the contact it belongs to.

It now calls LogContact, which is what N1MM calls at the same point. A
station already worked is logged again as a dupe, worth no points, which is
what the WAE rules expect: the second contact does not count but the traffic
does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 19:30:54 +00:00
erikbrockoandClaude Opus 5 3e326053a0 Say how many QTCs are left with the station being worked
WAE gives a point for every QTC line, so an operator needs to know what is
still there to exchange. The entry window now carries N1MM's QTCinfo line in
WAE: how many QTC lines have already gone with the station in the callsign
box, how many more may go, and how many contacts are left to report to
anybody. The QTC window shows the same line.

The numbers come from the log rows, the way N1MM works them out:

- exchanged is every QTC row with that call, both directions and all bands,
  and ten is the lot
- available is every contact not reported to anybody yet, without the dupes
  and without the station itself, which is N1MM's QTCRecordsetCount
- what is left with the station is what is free of its ten, and no more than
  the contacts there are to report when we are the one reporting

Europe on CW and SSB only takes traffic down, so its line leaves the last
number out, as N1MM's does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 19:30:25 +00:00
erikbrockoandClaude Opus 5 f2d9e331b4 Give each operator his windows back
The program stored an operator's window positions but never used them at
startup: the entry window was not placed and no window was reopened. It
now restores the whole screen both at startup and on a change of
operator: the windows that operator had open, where he had them, and the
ones he did not have closed.

The second radio's entry window no longer saves the layout. It has none
of the other windows open, so saving from it dropped every window the
first one had.

Ctrl+O gets six buttons for the operators last at the radio, newest
first. One click changes to that operator.

The recordings folder is no longer asked for. It is `Wav/<callsign>`
under the data folder, which is where N1MM plays them from, so
`StoredOperator.WavPath` is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 13:58:12 +00:00
erikbrockoandClaude Opus 5 d5788da240 Wipe on Alt+W as well as put the boxes back
Alt+W only put the boxes back, so wiping needed Ctrl+W or the button.
It now goes both ways, the way N1MM's Wipe button does: with something in
the boxes it clears them, and pressed again on empty boxes it puts back
what it cleared.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-10 11:30:07 +00:00
erikbrockoandClaude Opus 5 24542b8355 Put the wiped entry boxes back on Alt+W
N1MM wipes on Ctrl+W and puts the boxes back on Alt+W, which this had no
key for. A wipe now keeps the call, the exchange, the name, the comment
and the sent time, and Alt+W fills them in again.

Wiping empty boxes keeps what the wipe before it held, and the boxes are
only filled in again when they are empty, so nothing typed since the wipe
is thrown away. A call moved onto the call stack is not kept: it has not
gone anywhere and putting it back would make a second copy of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-10 11:17:41 +00:00
erikbrockoandClaude Opus 5 ab9730b26d Check the entry boxes by the rules the log window uses
The entry window took any text and only the log window checked it, so the
same value was accepted in one place and refused in the other. The rules
now live in one file, ExchangeCheck, which both windows call.

OperatingPosition.EntryProblem names the first box that cannot be logged
and why. LogContact and SaveQuickEdit throw on one, so a caller that has
not checked cannot write nonsense to the log. The entry window checks
before it logs, puts the reason in the status line and the cursor in the
box to fix, which covers Enter, the Log It button, ESM and {LOG}.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-10 11:13:38 +00:00
erikbrockoandClaude Opus 5 7ca2c5cd52 Walk the QTC window the way N1MM walks it
Four changes to the QTC window, three of them N1MM's behaviour and one an
addition.

Tab, enter and space all move to the next box the operator types in, and shift
and tab move back. The walk covers those boxes and nothing else. N1MM handles
the three keys in every box itself — `QTCLinesA_KeyDown` and its two siblings,
with `PreviewKeyDown` claiming Tab — rather than leaving them to the tab order,
so the cursor never lands on a switched-off line or on a button beside one.
Tab was going to Avalonia's default walk, which covered every control in the
window.

The header now says how many lines take traffic. `QTCHeader_LostFocus` reads
the count after the last `/` and hands it to `ColorizeCWRXFields`, which enables
that many lines and switches off the rest, capped by how many of the station's
ten are still free. All ten were live whatever the header said. Nothing already
typed is cleared, which N1MM does under `DontErase`.

A time typed short is filled in from the line above as the cursor leaves the
box: `52` under `1148` reads as `1152`. `QTCLinesA_LostFocus` takes as many
characters off the front of the line above as the typed part is missing, and
drops a colon whatever the length. `QtcTraffic.FillTime` is that rule.

The addition: while the cursor is in a QTC callsign box the check window
answers for that call rather than the entry window's, so a call taken down in
traffic is checked the same way one being worked is. `CheckWindowSources`
takes the call from `Elsewhere` when the QTC window has set it.

Checked by driving the window: header `4/5` left five lines live and read
"1 of 5", tab walked header to time to call to serial to the next line's time,
shift and tab walked back, `52` on the second line became `1152`, and the check
window listed W1AW while `W1A` was typed in a QTC line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-09 21:17:03 +00:00
erikbrockoandClaude Opus 5 bae40bb04e Show the boxes N1MM shows for each contest
The entry window had a fixed set of boxes with a Name and Comment box beside
the exchange. N1MM lays out one row of boxes per contest and nothing else, so
this does the same: general logging keeps Name and Comment, and a contest gets
the boxes it exchanges.

Two new members on `Contest` carry the cases where the boxes and the log
columns are not the same list:

- `FromExchange1` spreads what was typed in one box across the log columns.
  Where the other station sends a serial or a district code depending on where
  it is, N1MM gives it one box and sorts the value into the number, section or
  zone column from there, so the score and the Cabrillo file read it from the
  same column N1MM does.
- `LogFieldsFor` gives the log window its own column list. Sweepstakes takes
  the whole exchange in one box and logs it in four columns; `SweepstakesExchange`
  parses `72 A 74 SCV` in any order, reading from the last word back as N1MM
  does.

The sent serial is now marked as not required and space steps over it along
with the two report boxes, because nobody types the number going out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-09 20:12:37 +00:00
erikbrockoandClaude Opus 5 4eb8071d57 Say when the entry has two signals on the air
N1MM blocks the transmitter when a multi-operator entry is about to break
its own class. This program does not touch the transmitter: a logger that
decides for itself when the operator may not send loses a contact every
time it is wrong, and it is wrong whenever it has stale information about
the other stations — which, on a network, is whenever a laptop has just
been unplugged.

So the entry window says so instead. While this station is transmitting
and the entry is over what it is allowed, a red box goes across the top of
the window naming the station and the band. It is above the frequency and
under the menu, so nothing it covers is anything the operator needs while
he is sending, and it is big enough to read from the other side of the
room.

`TransmitRules` is what it asks:

| Entry | Allowed |
|---|---|
| one transmitter | one signal |
| two transmitters | two signals, on different bands |
| anything else | one signal per band |

Two on one band is wrong in every multi-operator class, so it is checked
whatever the category says. A station that has not said which band it is on
counts as somewhere else: a guess there puts the box in front of an
operator doing nothing wrong.

This is also the first thing that sends `XMIT`. The message was written and
nothing called it, so the transmit column of everybody's network status
window stayed empty and no station knew when another was sending. The entry
window now says when its key goes down and what it is sending.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 23:23:46 +00:00
erikbrockoandClaude Opus 5 871a28c10f Take a clicked word into the box the cursor is in
GrabIntoFocused puts text in whichever box has the cursor, whatever kind of
value that box normally holds. It is what N1MM's digital window does on
Ctrl+click, for the exchange element the contest has no box for.

The entry window also says when a CQ has gone out, which the digital window
needs to clear its grab list on: a run that starts again brings new callers.

GrabList.N1mmDepth is the ten calls N1MM's grab window holds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-09-01 00:03:08 +00:00
erikbrockoandClaude Opus 5 654adda080 Keep an operator's windows, colours and recordings
Ctrl+O asks who is at the radio, as it does in N1MM, and the callsign goes on
every contact logged from then on. A multi-operator station changes hands every
few hours, so each operator also keeps what he wants the program to look like:
where the windows sit, the theme, and the folder his recordings are in.

The windows on the screen are stored against the operator leaving, before the
dialog opens, and the operator taking over gets his own back. A window he opens
later is placed as it appears. An operator who is new to the list starts with
whatever is on the screen.

Nothing plays the recordings yet, so the folder is stored and waits on voice
keying. {OPERATOR} in a message sends the callsign.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 22:28:18 +00:00
erikbrockoandClaude Opus 5 c8de74e24a Add the digital interface, running MMTTY under Wine
MMTTY and 2Tone have no socket or pipe interface: N1MM hosts XMMT.ocx and
exchanges window messages with the engine. A Linux process cannot load that
control, so bridge/nonemm-mmtty-bridge.exe hosts it under Wine and passes
lines over its standard input and output. docs/digital-bridge.md states the
protocol and what the control needs.

The window is N1MM's: receive pane with coloured callsigns, grab list, call
stacking, twenty-four macro buttons and the engine controls. One left click
copies what is under it — a callsign to the callsign box, anything else to the
exchange box the contest keeps for that kind of value.

Config > Digital registers XMMT.ocx in the Wine prefix on its own, making the
prefix first if it is not there. The engine, the bridge and the control start
at the copies shipped beside the program.

The bridge reads the control's own events. OnTranslateMessage carries only the
messages the control has no event for, so nothing was ever decoded through it.

hamlib's data mode names read as digital modes now, and a mode typed into the
callsign box changes mode the way a frequency changes band, so a station with
no radio can reach RTTY at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 21:35:45 +00:00
erikbrockoandClaude Opus 5 2eeda451c3 Count down the band change timer in the status bar
SOBandChangeTimerDuration and MOBandChangeTimerDuration say how long a
station has to stay on a band it has just moved to. Both were read by
nothing: N1MM counts them down in its info window, which this program
does not have.

The countdown now stands in the entry window's status bar, next to the
contacts and multipliers, ticking with the clock that is already there.
BandChangeRules.StayLeft works it out from the log, starting the stay at
the minute after the contact that changed band, which is where N1MM
starts it.

Which contests show it is decided at runtime rather than by a list: the
rules come from the contest for the entry's own category, so a file that
asks for no stay, or asks for one only of single-op entries while the
station is entered multi-op, shows nothing at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 13:51:17 +00:00
erikbrockoandClaude Opus 5 921033cd93 Carry alternating CQ on after a contact
Alternating CQ ran off every message the keyer finished, so working a
station that answered started a CQ on the other radio, and Escape --
which stopped it outright -- was the way around that. It now follows
N1MM: only the CQ message carries the alternation to the other radio,
anything else holds it where it is, and so does Escape. The next CQ
picks it up again, which is what the CQ after a logged contact does, so
Ctrl+B is needed once rather than after every contact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 13:29:44 +00:00
erikbrockoandClaude Opus 5 dc1c593b4d Send WAE QTC traffic on CW
The QTC window took traffic down and read it out of the log, but nothing left
the window: on CW the operator had to send the lines by hand. It now puts them
on the air through the entry window's keyer, the way N1MM's QTC window sends
through its own entry window, so the same macros are expanded and the same
transmitter is pointed at first.

Sending a series: Send Hdr keys the header, Send1 to Send10 key one line each —
the time, the callsign and the serial number — and Close keys the TU message
when one is set. Taking one down: Hdr Agn asks for the header again, and shift
with Enter in a box asks for that one field again and clears it.

The messages and their defaults are N1MM's: AGN TIME, AGN CALL, AGN SERIAL, the
hard-coded HDR AGN with its two spaces, and a TU that sends nothing until the
operator writes one. So is the field spacing, written N1MM's way with S for a
space. All five are in Setup, where the note about the missing settings used to
be.

Nothing is sent on SSB or RTTY. N1MM plays four recordings on SSB and sends
RTTY from its digital window, and there is neither here, so on those modes the
buttons move the cursor as before. RX Ready sends nothing on any mode, because
a recording is all N1MM ever sends from it. Serial numbers go out as digits:
N1MM can send cut numbers and nothing here does, in any message.

Driven under Xvfb against a fake cwdaemon, in a WAE CW contest with four
contacts logged. Sending, it keyed QTC 1/2, 2257 DL1AAA 01, 2257 DL2BBB 01,
then QTC 2/2 with its two lines and TU DE W1ABC on close. Receiving, HDR  AGN,
AGN TIME, AGN CALL and AGN SERIAL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-30 23:03:17 +00:00
erikbrockoandClaude Opus 5 ff46cb00ed Put N1MM's status bar along the bottom of the entry window
Three parts, as N1MM has them: what just happened on the left, the contacts
and multipliers in the middle, the score on the right.

The left says where the call being typed is from, in N1MM's own shape:
OM: EU -> Slovak Republic, Zn 15. The zone is the one the contest counts in, so
Contest.UsesItuZones answers that the way N1MM's ZoneType does, and IARU
overrides it. A message from something the operator just did stands there until
the next keystroke.

The middle is N1MM's breakdown: the contacts and then one number per multiplier
the contest counts, separated by slashes, and No Score before the first
contact. Points are not in it, because they are not in N1MM's; the score window
has them.

Those numbers were in the line at the top of the window, which now carries the
contest name alone, and the country was in the bar under the callsign box,
which now says only what working the call would bring. Neither is said twice.

Looked at under Xvfb: the country line for OM3KFF and then G4AB, 10/7/8 and
300 beside them, and a Mark message standing in the left until the next
keystroke.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-30 22:46:13 +00:00
erikbrockoandClaude Opus 5 9b84b103ac Add N1MM's Mark and Store buttons
Both put something on our own bandmap and nothing on the cluster, which is
what separates them from Spot It.

Mark, Alt+M, leaves a mark where the radio is to say the frequency is busy.
The label is N1MM's, Busy@ and the time. It is not a station, so Spot carries
IsStation: nothing judges it, the check window does not offer it as a call,
available mults and Qs leaves it out, space does not take it into the callsign
box, and the bandmap paints it like a station already worked, which is how
N1MM marks it. The station count under the bandmap counts stations.

Store, Alt+O, puts the call being typed on the bandmap at this frequency so it
can be worked later. N1MM writes "Local spot" in the comment and this does the
same.

The action row now holds the eight buttons N1MM has, in N1MM's order and at
N1MM's widths: 23 per cent to Esc: Stop and 11 to each of the rest.

Left out of Store: N1MM also adds the call to what the check window's Master
column offers for the rest of the session. The call is on the bandmap, which
the check window already reads, so it is offered either way.

Driven under Xvfb: Alt+M leaving BUSY@22:32:27 on 14000, Alt+O storing
SP9XYZ there, and both drawn on the bandmap in the worked colour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-30 22:34:32 +00:00
erikbrockoandClaude Opus 5 ec8cdb88d7 Stack the calls that answer a CQ, the way N1MM does
A CQ is answered by more than one station. N1MM lets the operator put the
others on a stack and work them one after another; this is that, with N1MM's
keys and macros.

Ctrl+Alt+G and {STACKANOTHER} put the call being typed on the stack and clear
the boxes. Alt+G brings the top call back. Alt+D drops it. {SOCALLSTACK} both
stacks and unstacks in one key: it swaps the typed call for the top one, stacks
it when the stack is empty, and takes the top call when the boxes are.
{LOGTHENPOP} and {LOGTHENNEXT} log the contact and bring the next caller in.
{CLRSTACK} empties it.

The rules are N1MM's, and CallStack and the entry actions hold them without a
window, so they are unit-tested: stacking only while running, nothing shorter
than two characters, no dupe, no frequency typed into the callsign box, a call
already on the stack moved rather than repeated, and a multiplier to the front.
{SOCALLSTACK} takes the stacked call off before it puts the typed one on, so a
multiplier just typed cannot come straight back.

Two places differ. N1MM refuses your own call only while the stack is empty;
it is never a station to work, so it is refused either way here. And the order
is multipliers first in every mode: N1MM's digital window offers first-in and
last-in as well, and there is no digital window here.

The stack window opens by itself with the first call and closes with the last,
or Window > Call Stack opens it. It does not take the keyboard when it opens,
because the operator is typing the next call. The next call out is ringed and
each is coloured by what working it would bring, as in every other window.

OperatingPosition.JudgeCall now answers what a call alone is worth, which the
stack window and the check window both need; it was private to the check
window before.

Driven under Xvfb: four calls stacked with Ctrl+Alt+G and listed in the window,
Alt+G bringing the top one into the boxes, Alt+D dropping the next.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-30 22:23:30 +00:00
erikbrockoandClaude Opus 5 bb35848e11 Lay the buttons out the way N1MM lays them out
The twelve function keys were one row of twelve at the bottom of the window,
with the action row under them. N1MM puts both between the entry boxes and the
bearing line, six keys to a row in two rows, each row filling the width.

The action row takes N1MM's widths as well: 23 per cent to the first button and
the rest shared out. N1MM gives the remaining seven 11 per cent each; there are
five here, because Mark and Store are missing, so they share the same 77.

Looked at under Xvfb against the N1MM screenshot: same two rows of six, same
action row under them, same order down the window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-30 22:07:11 +00:00
erikbrockoandClaude Opus 5 13a1acfcab Lay the band panel out the way N1MM lays it out, and colour it
N1MM's band panel is a row per band and a column per mode the contest runs,
with every cell painted by what the callsign being typed would be worth there:
red for one new multiplier, green for more than one, blue for points, grey for
a station already worked on that band and mode. With nothing typed every cell
is grey. That is the panel's point — it says where a station is still needed
without working the bands one at a time — and this had only the frame.

BandPanel holds the rule and is unit-tested without a window. The bands are the
six contest bands, with 30, 17 and 12 metres added for general logging: in N1MM
that is the one entry of the hundred and thirty-five that shows the WARC bands,
so Contest.ShowsWarcBands says so and GeneralLogging overrides it. The columns
are the modes the contest runs, so CQ WW CW has one column and IARU has two,
rather than always showing CW and phone. A digital entry gets one DIG column
where N1MM has RTTY and PSK: dupes and points here go by mode category, and
those two are the same category.

The labels are N1MM's: the metres alone up to 10, then the unit from 6m up. The
band the radio is on is ringed rather than filled, as N1MM rings it, so the
ring does not hide the colour underneath. The cells are labels with a click
handler rather than buttons, so the theme cannot paint over the colour under
the pointer.

Text on a filled cell is black or white by N1MM's rule: light is the three
channels adding up to more than 384.

Looked at under Xvfb in CQ WW CW: one CW column, six bands, grey with nothing
typed, and with a JA typed red on every band but 20, where the zone and country
are already claimed, and which carries the ring.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-30 21:42:45 +00:00
erikbrockoandClaude Opus 5 da04764756 Two themes, and one place that decides what colour a station is
Colours came from static brushes scattered over the windows. They now come
from a Theme, and everything that paints a station, a spot or a log row
asks Themes.Current as it paints, so changing the theme changes every
window without reopening it.

light is N1MM's own palette, read out of ApplicationStyles: the pale blue
behind a window, ivory on a button, white boxes, and the four colours a
station is painted in. dark is the same states in colours for a dark
screen, which is roughly what the program looked like before.

Config > Manage Skins, Colors and Fonts picks one, under N1MM's name for
the same dialog, and shows what each theme paints a callsign. The choice
is kept in settings.json as Theme, and light is the default.

The window chrome follows through resources the Fluent controls read, so a
text box and a button take the theme's colours in every state. Fonts and
skins are not part of it.

Looked at under Xvfb in both themes: the entry window, the theme dialog,
and the log window with its rows still coloured by what each contact was
worth.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 13:06:32 +00:00
erikbrockoandClaude Opus 5 e4b3dc9572 Colour the callsign the way N1MM colours it, and mark the call
The colours are N1MM's own defaults, read out of ApplicationStyles: blue
for a station worth working, red for one new multiplier, green for more
than one, grey for a dupe, and red on the CQ frequency line. A call under
three characters stays blue, as N1MM leaves it.

The mark at the right-hand end of the callsign box is N1MM's OKCall: a
tick when the call is in MASTER.SCP, a question mark while it is not, in
the colour of the call. Like N1MM it only shows while the check window is
open, for three characters or more, and only while the call is short
enough to leave room for it.

The band under the boxes takes N1MM's background colours instead of white
on the text colours. The two lights, the telnet window and the message
editor were borrowing the verdict brushes for things that are not
verdicts; they have their own colours now.

The callsign and the exchange boxes uppercase what is typed. Name and
comment do not, which is what N1MM does.

Scored contest 63 of om5m.s3db (CQ WW SSB, 3775 QSOs) against the points
and multiplier flags N1MM stored: points identical, multipliers 601
against 602. The difference is B1Z on 10m, which N1MM logged with an
empty country prefix and counted as a country of its own though BY was
already worked on that band.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 12:50:56 +00:00
erikbrockoandClaude Opus 5 1c118f9ce4 Lay the menu bar out the way N1MM lays it out, and fill in the Edit menu
The menu tree comes from N1MM's EntryWindow.Designer.cs: File, Edit, View,
Tools, Config, Window, with the items under the names N1MM gives them. The
window list moved out of View into Window; the call history file moved from
Config into File > Import. Ctrl+W, Ctrl+L and Ctrl+M now work, so the keys
printed in the menu are real. Two commands that had no menu item before:
the CW and SSB function key definitions, and the QTC setup area.

The rest of the Edit menu:

- Ctrl+N asks for a note and puts it in the comment of the contact in the
  boxes, or of the last logged contact when nothing is typed.
- Edit Current Contact opens the Edit Contact form over what is typed but
  not logged. Update hands the values back to the entry boxes.
- Ctrl+Q and Ctrl+A load a logged contact into the boxes and paint them
  pale yellow. Enter writes the change back, Esc puts back what was being
  typed, and stepping forward past the newest contact leaves quick edit.
- Ctrl+U raises the received serial, or a numeric exchange box when the
  contest has no serial.
- Ctrl+F shows the call being typed in the log window, then the next
  contact with that call.

RadioPosition is now OperatingPosition: it is the operator at one radio,
not a place on a band.

Looked at under Xvfb on a 3775-contact log: quick edit back and forward,
Esc, the note dialog, find, and Edit Current Contact putting a zone back
into the entry boxes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 12:18:37 +00:00
erikbrockoandClaude Opus 5 682932c35d Make the run switch a switch, and split the tuning tolerance by mode
Three things you could not see or could not set.

Run and S&P were a label saying which one you were in. They are two buttons
now, the one you are in marked, either one clickable, which is what N1MM has.
Ctrl+? and F1 still work.

The tuning tolerance is one number per mode, as N1MM keeps it: CW, phone and
digital, 300 Hz each, which are its defaults. A phone signal is wide and a CW
one is not, so one number for both was wrong.

The bearing line went blank whenever it could not be worked out, which looks
exactly like a missing feature. It now says which end could not be placed and
what to do — no country file, no position for your own station, or a callsign
the country file does not place. It also gives miles beside the kilometres, as
N1MM does.

While clicking it: a marked button lost its mark under the pointer, because the
theme paints its own background there. The band buttons had the same fault.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 10:39:32 +00:00
erikbrockoandClaude Opus 5 7e7ce41477 Let tab walk every box again
Tab was doing what space does, which is wrong. N1MM's EntryWindow does not
touch the tab key — ProcessCmdKey only remaps NumPad Enter — so tab there is
plain navigation through the boxes in the order they are laid out: the reports
at tab index 3 and 4, the name at 26, the section at 30, the zone at 32, the
comment at 40. Only space goes through NextTab, which is the walk that steps
over the reports.

So tab now walks every box, reports, name and comment included, and shift and
tab walk back. Space is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 10:32:38 +00:00
erikbrockoandClaude Opus 5 8956c2fd22 Take the rest of what reading N1MM turned up
The call frame. Above the callsign box, N1MM writes which spotted station the
radio is sitting on, coloured like the bandmap colours it, or CQ-Frequency when
this is the frequency the last CQ went out on. Space with an empty callsign box
takes the call out of the frame and into the box, but only while searching:
TabPressed does that when the run box is unchecked, because a running station is
answering callers rather than chasing the one under its VFO. How close counts as
sitting on it is N1MM's tuning tolerance, 300 Hz, now a setting.

Sh/dx while unassisted. TelnetClient.Output refuses any command holding SH/DX
when the entry is not assisted, and the telnet window does the same now.
Whether an entry is assisted follows N1MM's own rule: assisted, multi-operator
or checklog may use spots, a single operator who did not say assisted may not.

The node's flavour. Click_LogonButton reads DXSpider, AR-Cluster, CC Cluster or
GoCluster out of the banner; ClusterClient reads the same words and the window
shows what it found. It changes nothing yet — the commands it would change are
ones this does not send — but the node says so and now we know.

CQ WW RTTY's state box. NextTab only stops on SectionText when the station is
US or Canadian, because nobody else sends a state, so a contest can now say a
box is not for this station and the walk steps over it. The box is still there
to type in.

The telnet button editor shows as many buttons as are defined, with an empty row
to write the next one in, rather than N1MM's twelve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 10:23:30 +00:00
erikbrockoandClaude Opus 5 f102e0a304 Walk the entry boxes the way N1MM walks them
Space and tab were moving through every box in turn, and since the name and
comment boxes arrived, into those as well. N1MM does neither.

Its EntryWindow.NextTab walks the boxes the contest declares plus the callsign,
skips SNTText and RCVText, and comes round to the callsign from the last one.
For CQ WW that means the callsign and the zone, and nothing else: the entry
items are SNTText, RCVText and CQZoneText, and the first two are stepped over.
So this steps over report boxes as well, and the name and comment boxes are out
of the walk, as they are out of N1MM's.

Skipping the reports only works because they are filled in. N1MM's TabPressed
puts the report for the mode into an empty Snt or Rcv box when the callsign box
is left, and FillReports does the same here: 599 on CW and digital, 59 on phone.
A report the operator typed is left alone, and a report box is still there to
click when a contact is not 599.

The test that said space walks every box in turn now says it steps over the
report, which is the behaviour that was asked for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 10:04:48 +00:00
erikbrockoandClaude Opus 5 7c2a2ed768 Add the name, comment and lights from N1MM's entry window
Three more things off the screenshot of N1MM's entry window.

A Name box and a Comment box beside the exchange. Neither is part of the
exchange, so an empty one does not hold up a contact and ESM does not wait for
it; both fill the log columns of those names, and a contest that exchanges a
name keeps what was exchanged. Tab now walks the callsign, the exchange, the
name and the comment before coming round again, which is what the boxes being
there implies. The name is filled from the call history file, and what that file
says in its UserText column is written under the entry boxes, where N1MM writes
it.

Two lights beside the run switch: green while a radio is answering, red while
something is going out on the air. The transmit light follows the keyer, so with
cwdaemon it goes out when the message has actually finished rather than when the
text was handed over.

The window is wider to fit the two new boxes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 09:13:58 +00:00
erikbrockoandClaude Opus 5 f8e07471a6 Add the button row and the title bar N1MM has
The title bar said Nonemm. It now says what N1MM's says: the frequency, the
mode, whether the frequency came from a radio or was typed, and which radio the
window belongs to when there are two. Reading it off the taskbar is the point.

Under the function keys is N1MM's second row: Esc: Stop, Wipe, Log It, Edit,
Spot It and QRZ. Each one does what its key already did, so the keys and the
buttons cannot disagree. QRZ opens the callsign's page in a browser, and says so
in the status line when there is no browser to open.

Mark and Store are left out. They stack callsigns, and call stacking is not
here, so a button that did nothing would be worse than no button.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 09:07:45 +00:00
erikbrockoandClaude Opus 5 86d1f71ffe 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>
2026-08-28 08:45:42 +00:00
erikbrockoandClaude Opus 5 a06bbde3f9 Say where the station being worked is
N1MM writes a line under its entry window with the beam heading, the heading the
long way round, the distance and the sun times at the other end. This writes the
same line.

SunTimes is the published sunrise equation, with the usual -0.833 degrees for
the sun's radius and refraction at the horizon, and it returns nothing on the
days inside the polar circles when the sun does not rise or set. It agrees with
published times for London at both solstices, Sydney in June and the equator at
the equinox, to within three minutes.

StationPath picks the position: the grid square when the contest exchanges one
and it has been copied, then the call history file, then the country file, which
puts the station in the middle of its country. That is close enough to point a
beam with. Our own end comes from the station grid square, or from the country
file for our own call.

One thing to know when reading the times: they are that station's own sunrise
and sunset in UTC, so for Japan the sun rises late the evening before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:42:07 +00:00
erikbrockoandClaude Opus 5 cf0166f1b2 Edit the function key messages as their file
The messages were twelve boxes in a dialog, which meant no labels, no
search-and-pounce set, and no way to read a file somebody published. They are
now the file itself: right-click any function key button under the entry
window, or the two buttons in Config ▸ Keyer and messages, and the text of an
N1MM .mc file opens in a plain editor with Import, Export and Back to the
defaults.

MessageFile reads that format by N1MM's rules. A line starting with # is a
comment; every other line is a label, a comma and the message, with && standing
for one & in a label. Which key a line belongs to is decided by where it is and
nothing else: the first twelve lines are F1 to F12 while running, the next
twelve the same keys while searching, and a file that stops part way through the
second twelve leaves the rest sending what they send while running. A blank line
in the middle is a key with nothing in it, which is what the manual says; the
newline that ends the last line is not, which it does not say, but no .mc file
in the world means its final newline as a message.

Two things fall out of this. The buttons now say what the file says, and they
change when the operator moves between running and searching, so the labels are
the documentation N1MM's manual suggests writing them as. And the search set is
real: ESM's F2 while searching can be a different message from the one it sends
while running, which is how the file was always meant to be used.

Settings keep the file text and carry the twelve stored messages into it on
first read, with the labels this program used then, so nobody loses what they
had typed.

The editor is deliberately a text box rather than a grid of fields: importing,
exporting and editing are then the same thing, and the comments an operator
writes in the file survive being edited here. The telnet window's buttons can
use the same editor later.

Running it caught two: Save as the default button ate the Enter key that a
multi-line editor needs, and the button labels did not follow the move between
run and search until they were redrawn rather than only rebuilt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:13:24 +00:00
erikbrockoandClaude Opus 5 0c440ed865 Let Enter send the message the contact has got to
ESM is how most people run N1MM, and it was not here at all — not in the code,
not even in the list of what is missing. Config ▸ ESM turns it on and it stays
on between runs.

The decision is a table in N1MM's function-key documentation, and it is written
here as one: the state of the callsign and exchange boxes, whether the station
is running or searching, and whether the call and the exchange have gone out
already, give the function keys Enter sends and whether the contact is logged
after them. Esm.Decide is that table and nothing else, so it is tested against
every row rather than by clicking.

Searching: type a call, Enter sends your call, space moves to the exchange, and
once the exchange is filled in Enter sends yours and logs. Running: Enter calls
CQ, a call in the box makes Enter send his call and the exchange, and the next
Enter ends the contact and logs it. A dupe gets QSO B4 while running and nothing
while searching, unless dupes are worked, which is what N1MM recommends and what
this does out of the box. F1 puts a searching station into run mode.

The entry window highlights the keys Enter would send next, so what is about to
happen is on the screen rather than in the operator's head. `=` sends whatever
Enter last sent. Escape and F12 put ESM back to the start of a contact.

Send Corrected Call is in as well: copy SM3AB, send it, fix it to SM3ABC, and
the message that ends the contact goes out as "SM3ABC TU DL1ABC". That is why
N1MM's documentation says to put ! in F5 rather than {CALL}, so the default F5
message is now ! . F6 is QSO B4, which is the message ESM sends a dupe; it used
to repeat the exchange, which no part of the table asks for.

{EXCHSENT} means something again, now that there is an ESM state for it to set.

Worked end to end against a fake cwdaemon: a search-and-pounce contact logged in
four keystrokes, a run contact in three, both with the right text on the keyer
in the right order, and the corrected call in front of the last message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 07:59:11 +00:00
erikbrockoandClaude Opus 5 333d3b97c2 Run the action macros in a function key message
A function key message was read as text and nothing else, so a message file
written for N1MM lost half of what it said: {WIPE}, {LOG}, {RUN} and the rest
expanded to nothing and the key only sent characters.

Reading a message now produces a plan rather than a string. MessagePlan.Read
walks the template once and sorts what it finds into three parts: the actions
that run before anything goes out, the text that goes on the air, and the
actions that wait until it has gone. N1MM's {END} decides which side an action
falls on, and text after {END} is dropped, because the message is over by then.
So TU {LOG}{END}{WIPE} logs the contact, sends TU, and clears the boxes when the
key has finished sending.

The actions that are carried out: {WIPE}, {LOG}, {RUN}, {S&P}, {SPACE},
{SPOTME}, {STOPTX}, {FREQUP}, {FREQDN}, {PGUP}, {PGDN}, {JUMPRX}, {JUMPRXTX},
{CTRLF1} to {CTRLF12} for sending on the other radio, {TELNET sh/dx} for a
command to the cluster node, and {OTRSP TX2} for one to the SO2R box. The last
one wanted a way to send a command straight to the box, so So2rBox grew one.
The frequency steps are two settings, with N1MM's own two numbers behind them.

Waiting on the keyer is the part worth reading twice. What follows {END} runs
when the keyer says the message has gone out, which is the only honest moment:
timing it from the length of the text is a guess that goes wrong exactly when
the contest is busy. A keyer that does not report completion is taken at its
word as soon as the text is handed over, and one that goes quiet is given two
minutes before the actions run anyway, because never running them is worse.

Running it against a fake cwdaemon and a fake node showed the message text on
the keyer, the telnet command at the node, RUN lighting up before the message
and the boxes clearing after it. That is also what showed the entry window was
not redrawing between the actions and the message.

What is still read and passed over is written down: the CAT, audio, rotator,
stereo and call-stacking families, the digital TNC macros, and the CW keyer
characters < > ~ ] [ + = , which belong to the keyer rather than to the
expander. {EXCHSENT} waits on ESM, which is next.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 07:44:59 +00:00
erikbrockoandClaude Opus 5 d9f9d880f2 Give the telnet window the rest of what N1MM's has
The packet window showed traffic and took a command line, and everything else
about the cluster lived in a dialog under Config. It is now the window N1MM has,
with the same five tabs.

Telnet shows the traffic with spot lines in green, what went out in blue and
lines from a preferred spotter in bold. Double clicking a spot line, or "Jump to
this spot", puts the radio there with the call in the entry window. Scrolling
stops while the pointer is over the traffic. The client keeps the last two
hundred lines, so a window opened mid-contest is not blank.

Clusters keeps the operator's nodes with their ports, passwords and after-login
commands, connects and disconnects, and holds the logon settings. Download
fetches the published list of telnet nodes from NG3K — around fifty, with the
sysop's call and a note about each — and clicking one fills the boxes in. N1MM
downloads its list from its own web service, which asks the operator to opt in
to data collection and is N1MM's to run, so this reads a public page instead.
ClusterList takes any page with telnet:// links in a table; a page it cannot
read leaves the stored list alone.

Filters decide which spots reach the bandmap: bands, modes, beacons, busted
calls, stations outside the call history file, blacklisted spotters and calls,
spots from outside your country, continent or a list of prefixes, and how long a
spot stays on the map. A busted spot is a call the callsign database has never
heard that is one character away from one it knows; a call nothing resembles is
kept, because that is what a new station looks like. Nothing is filtered out of
the traffic itself — the operator sees everything the node sends.

Buttons edits the twelve command buttons. A button takes what N1MM's takes: the
message macros, several commands separated by semicolons, or {CONN} and the name
of a favourite, which connects to that node instead of sending anything. The
label takes the macros too. Right-clicking a button opens the editor.

Config ▸ Cluster now opens this window on the Clusters tab rather than a dialog
of its own, which is where N1MM keeps those settings.

Three things that could take the program down while a cluster was connected:

Settings.Load read a null where the property is not nullable. A file that names
a key with a null value — one written before the property existed and then
edited — put that null straight through, because the property's own default only
runs when the key is missing. Opening the telnet window then threw on the first
list it touched. Every null is now put back to the default the property
declares, walking into the stored records and the lists of them.

Bandmap was written from the cluster's thread and read from the window's, so a
dictionary could be modified while a window enumerated it. Every method locks
now.

ClusterClient disposed its token source while its own loop still used it, and
the retry delay sat outside the catch, so a disconnect faulted the loop task.

Along the way the message macros were checked against N1MM's function-key
documentation, and several were wrong. {LOGGEDCALL} is N1MM's {LASTCALL}, the
serial is #, and there is no {MYZONE}; {NAME} and {GRIDSQUARE} stand for the
other station's name and grid, not ours; {OTHERMHZ} is the radio the operator is
not on. The single-character macros * and ! were missing. Added from the same
table: {LASTCALL}, {PREVNR}, {NAMEANDSPACE}, {CHNAME}, {GRID}, the two grid
bearings and the grid distance, {FREQ}, {FREQROUND}, the other-radio
frequencies, {TIMESTAMP} and {TIME2}. Frequencies are formatted the way N1MM
formats them, with R for the decimal point on CW. The macros that pass a station
to the other band take the second radio as a new argument, and stand for nothing
at a one-radio station.

Left out, and written down: saving spots to a database, which N1MM keeps in its
admin database rather than in the log file the two programs share; the
special-calls list; the two-character busted check, which is a few hundred
thousand lookups per spot against a few hundred for one character; and N1MM's
action macros, which need a different shape than an expander that returns a
string.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 07:33:55 +00:00
erikbrockoandClaude Opus 5 9ae5078c84 Take WAE QTC traffic down
Ctrl+Z opens the QTC window for the station being worked, which is the key N1MM
puts it on, and the window is laid out the way N1MM lays it out: the header,
ten lines of time, callsign and serial number, an Agn and a Cfm button beside
each, Clear, Close and Cancel underneath, and the same colours — green for a
line that will be saved, red for one still empty, yellow for one that cannot be
read. Enter and Tab move forward and space moves within a line, so a line can be
walked while listening to it.

Sending, the lines are filled from the log and cannot be edited: the contacts
not reported to anybody yet, oldest first, never the station being worked, and
never past the ten any station may have. The rule for what has been reported is
N1MM's — a contact counts as reported when a QTC row carries its call and the
serial we sent it — so the pair is kept in the row the way N1MM keeps it.

Which way traffic goes is not the operator's choice on CW and SSB: Europe
receives and the rest of the world sends. On RTTY it goes both ways, and there
the window carries a switch. That is the one piece of the workflow that is ours
rather than N1MM's, which toggles a QTC mode outside the window instead.

Nothing here transmits. N1MM sends the lines on CW and RTTY and plays recorded
voice messages on SSB; we have no QTC message templates, no voice keyer and no
digital window, so Agn, Cfm and RX Ready move the cursor and the operator sends
by hand.

Running it caught what the tests could not: focus set in the constructor does
not stick, so the first thing typed into a freshly opened window was dropped and
every field after it took the value of the one before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 00:02:47 +00:00
erikbrockoandClaude Opus 5 ae48c04e71 Alternate CQ between the two radios
Ctrl+B calls CQ on one radio, and when that message has gone out, moves to the
other and calls there. The keyboard, the entry window and the SO2R box follow
each turn. N1MM calls this dueling CQs and puts it on the same key.

The turn is taken when the keyer says the message has ended, not when a timer
guesses it has. So MessageSender grew a Finished event and a ReportsCompletion
flag, and both keyers fill them in: cwdaemon answers the <ESC>h reply request
that now goes out in front of every message, and a WinKeyer clears the busy bit
in the status bytes it sends of its own accord. The status-byte reading is in
WinkeyerStatus, away from the serial port, because that is the half that can be
tested without a keyer on the desk. A keyer that reports nothing refuses to
start alternating CQ rather than keying the second radio over the first.

AlternatingCq itself takes the keyer, a callback that calls CQ on a radio, the
gap and a wait function, so the alternation is tested without sleeping. The gap
is in Config ▸ Keyer and messages and will not go below 100 ms, which is N1MM's
floor too: an SO2R box works relays.

docs/keying.md writes down why cwdaemon does the timing and we do not. N1MM
keys DTR itself with a coarse sleep, a busy-wait and a margin that grows every
time the sleep overshoots, and it raises the thread to TIME_CRITICAL for the
length of the message. The busy-wait ports to Linux; the priority does not,
without CAP_SYS_NICE, and a garbage collection mid-element is audible. A direct
serial keyer stays a reasonable third option, to be taken knowingly.

Running it against a fake daemon that takes 1.5 seconds to play a message: six
CQs went out back to back and the keyboard moved between the two entry windows
each time. Escape stopped it, let the message in flight finish, and started
nothing further.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 23:28:57 +00:00
erikbrockoandClaude Opus 5 1fda1696f2 Read call history files, and fill the exchange from them
N1MM's format, so a file published for a contest is read as it is: # comments,
!!Order!! naming the columns of the lines after it, semicolons in preference to
commas so user text can hold a comma, and a default column order for a file
that names none. The directives that change what is stored are acted on;
!!Order!! can appear again part way through, because these files are usually
several files stuck together.

Leaving the callsign box fills what the file knows into the exchange boxes that
are still empty. A box with something in it is left alone: what the station
actually sends beats what somebody published months ago. The zone comes from
the CqZone or the ITUZone column depending on which the contest asks for, and a
file with no Sect falls back to State.

The check window's two missing columns are in. History lists matching calls
from the file. Exchange is not about callsigns at all: it offers the values the
exchange box with the cursor in it can hold, which is what N1MM's exchange pane
does — it searches the contest's list of valid exchanges, not the log.
CheckCandidate.Call is now Text, because an exchange value is not a call.

The section-validating directives are read and passed over rather than acted
on. Acting on them means holding N1MM's section lists and its rules about
retired sections, and getting that wrong throws away good data.

Running it caught what the unit tests could not: the exchange was filled and
scored but the boxes on screen still looked empty, because nothing resynced
them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 18:15:41 +00:00
erikbrockoandClaude Opus 5 1185faed78 Two entry windows and an SO2R box
LoggingSession mixed two things: the contest, which one station has one of,
and what the operator is typing, which belongs to a radio. It is now
ContestSession and RadioPosition. Two positions share one log, one score and
one run of serial numbers, so a station worked on radio 1 is a dupe on radio 2.
Frequency, mode, split, what is typed and whether you are running belong to
each radio on its own.

A second radio opens a second entry window. It has no menu of its own: it is
another view of the same contest, not another program. Only the window for
radio 1 opens the database and the connections.

Ctrl+Tab moves the operator to the other radio and the keyboard follows.
Ctrl+Shift+Tab puts both radios in the headphones.

OtrspBox speaks OTRSP to an SO2R box over a serial port: TX1/TX2 for the key,
RX1/RX2 and RX1S for the headphones, AUXnnn for an output line. It follows the
active radio, and every message points the box at this radio before keying, so
a message cannot go out of the radio the operator has just left. A command that
would change nothing is not sent, because the box works relays; N1MM does the
same. The box takes a Stream, so what it sends is tested without a serial port.

Still missing: alternating CQ, and voice keying on the second radio.

Looked at under Xvfb with two fake radios, one of them split: both windows up,
radio 1 showing 14008.00 into 14020.0, radio 2 on 14030.00, Ctrl+Tab moving the
keyboard between them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 16:49:24 +00:00