Commit Graph
100 Commits
Author SHA1 Message Date
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 9b3e10d127 Take a QTC header that is only a series number as ten lines
An operator copying a header off the air often types the series number and
nothing else. That read as a header the program could not parse, so the box
went yellow and Close refused to log the series.

N1MM fills the count in: "If he only types the batch number without the
forward slash and count, the program will assume the count is 10."

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 19:30:47 +00:00
erikbrockoandClaude Opus 5 265f90ccd7 Keep QTC traffic out of the serial numbers
A QTC row holds the serial of the contact it reports, which the station
being reported sent, not us. The serial pool was filled from every row in
the log, so reporting a station that sent us 1800 pushed our own next serial
to 1801, and reopening the log read it back from the rows again.

The pool now takes the sent number of contacts alone, when a contact is
logged, when one arrives from another station of the entry, and when the
numbers are seeded from the log at startup. N1MM leaves both QTC kinds out
of the same query, in QSONrsByBand.

The test opens a real log file, sends a series, closes the store and opens
it again, so the series numbering and the contacts already reported are
checked across a restart as well.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 19:30:32 +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 00bbb0e6b3 Mark the picked line in the log only while the window is in front
A marked line in a window behind says nothing about where the keyboard
is. The log takes the marks off as another window is activated and puts
them back when it comes forward; the contacts stay picked in between, so
"Change Operator…" and Delete still act on what the operator picked.

The frame around the cell the keyboard is on goes with them. The theme
draws that frame with a rectangle inside the cell, not with the cell's
own border, so it takes a style on the template.

Quick edit moves the picked line: Ctrl+Q and Ctrl+A step through the log
from the entry window, and the contact in the entry boxes is the picked
line here and is scrolled to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 17:22:54 +00:00
erikbrockoandClaude Opus 5 606c43e4a6 Take half the room either side of the text in a log cell
The values sit closer to the line between the columns, and a column that
is as wide as its values is fifteen pixels narrower.

A header takes much more room besides its text than a cell does — its
own padding, and the room the theme keeps for the sort arrow — so the
two are now measured with an allowance each. One allowance for both
clipped "Mode" and "CQ-Zone" to the column width the values needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 17:02:15 +00:00
erikbrockoandClaude Opus 5 836624376e Change the operator of the contacts picked in the log
A multi-operator station logs a whole shift under whoever was at the
radio, so a shift logged under the wrong callsign had to be put right a
contact at a time. "Change Operator…" in the log's right-click menu asks
for one callsign and writes it against every contact picked in the grid.

The dialog offers the operators the program already knows in a drop-down
and takes any other callsign typed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 16:56:04 +00:00
erikbrockoandClaude Opus 5 56c7bcef21 Fit more of the log on screen and edit a cell without fighting it
The theme gave each cell 32 pixels of height, which fits about seven
contacts in the window. A row is now as tall as its text and one pixel
either side, so about fifteen fit.

Editing a cell:

- the columns bind one way, and the value is written back when the edit
  is taken, on Enter or a click elsewhere. A two-way binding wrote on
  every key press, which took the edit as the first character was typed
  and closed the box.
- Enter on the current cell opens the box. The handler runs before the
  grid's own, which reads Enter as "move down a line".
- Left, Right, Home and End stay in the box. The grid read a key the box
  left alone as "go to the next cell", and the box leaves a key alone
  when the caret cannot move; End also landed at the start of the text.
- the box takes the height and the left inset of the text it covers, so
  nothing moves as it opens.

An edit no longer rebuilds the list. The lines on screen are given the
contact again, so the view stays where the operator put it instead of
jumping to the top and back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 16:55:59 +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 66e9a0ab65 Say what the serial number messages do
The README and the comments said the pool "goes out" and named it in
places where the reader cannot tell what is sent. They now name the
message and what it holds: the highest number taken on each band.

One of them was also wrong. The pool has not been sent on a reset since
the reset started counting from the log, and the README still said it was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-10 12:54:18 +00:00
erikbrockoandClaude Opus 5 980ddb586f Count from the log again on Reset Serial Numbers
The button started the numbers again at one. N1MM's manual says it "sets
serial numbers on all computers to the highest value in any of the logs,
plus 1", and its own handler re-reads the highest number per band out of
the log, so a mixed network came apart at exactly the point the button is
meant to repair: the N1MM stations counted on from the highest number and
this one started at one, sending numbers that had already gone out.

ContestSession.RestartNumbers fills the pool from the log, which is where
the reset now lands. SerialNumbersFrom is gone with it, and so is the pool
this station sent afterwards: N1MM sends none, because each station works
the same number out of its own copy of the log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-10 12:28:03 +00:00
erikbrockoandClaude Opus 5 7ba0b44fc1 Take serial numbers from a pool the whole entry shares
Each station worked out its next number as the highest in its log plus one,
so two operators typing at the same moment both sent the same number and
one of the two contacts was wrong in the sponsor's log.

SerialNumberPool holds the highest number taken per band, which is N1MM's
QSONumbers array. A number is taken at the first character of the callsign,
as N1MM takes it, and ReserveNr tells the other stations before the contact
is worked. A number at or below one this station has used is answered with
the whole pool, N1MM's QSONRS, and the station that is behind counts on from
there. The pool also goes out when a contest is opened and when the numbers
are started again.

Contest.NumbersByBand says whether the numbers run per band or as one
sequence for the station. CQ WPX runs one per band for a multi-two and a
multi-multi; every other contest here runs one sequence. The band index in
the message is N1MM's, so the array lines up with theirs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-10 11:56:49 +00:00
erikbrockoandClaude Opus 5 33c7631e17 Let the operator pick, order and sort the log columns
The log showed the contest's columns and nothing else, with the country,
points and multiplier columns fixed in the XAML. LogColumns now holds every
field the log can show and the settings file keeps the names, left to right.

A right click on a header adds or removes a column and offers the contest's
own set back, a header can be dragged to move its column, and a click sorts
on it. Sorting uses the field, not the text of the cell, so 9,000 kHz comes
before 14,000 and the serial numbers do not run 1, 10, 2. Both grids get the
same columns, so a value in the pane below sits under the column it belongs
to. Removing the last column is refused, otherwise nothing could be selected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-10 11:38:30 +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 95e1405be1 Check the signal report before it goes in the log
The log window took any text in the two report columns. It now checks the
report the same way it checks the zone or the section: RST on most modes,
and a signal-to-noise ratio in decibels on FT8, FT4, JT65, Q65 and MSK144.
A WAE QTC row is skipped, because it keeps the time sent and the other
station's call in those two columns.

The grid column is checked on every contest now, not only when the contest
asks for a locator, and the value is stored as JN88dd.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-10 11:07:09 +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 d4d0375f1f Get WAE ready: the score split, the Cabrillo layouts and the QTC list
The score summary now splits the way N1MM's does. `ContestLog` keeps a tally
per band, and per kind of row where the contest has kinds, and the score window
reads those instead of adding the rows up itself. Three things were wrong with
the old sum:

- The band rows counted a multiplier as one whatever the band. WAE weights it
  by band — `ScoreRow.AddContact` adds four on 3.5, three on 7 and two on 14,
  21 and 28 — so the band rows did not add up to the total.
- The band rows counted contacts left out of the claimed score.
- There was no split by kind. `CWAE.LogInfo` and `ScoreClass.Description` give
  WAE a `Q/QTC` column and a row per band and kind, so contacts, traffic sent
  and traffic received are read apart.

The Cabrillo writer now pads the record name to seven characters, which is what
`ConvertCabrilloStructStdToString` does and why: `X-QSO:` is six characters and
a space, so every record name puts the frequency in the same column. Nonemm
wrote five, and the `X-QSO:` lines added last commit did not line up with the
rest.

WAE's own layouts follow `CWAE.CabrilloString`: three spaces after the sent
serial rather than the one every other column gets, and the transmitter column
only for a multi-operator entry.

`QtcTraffic.ToSend` no longer offers a contact marked as a dupe, which is what
`DBSupport.QTCRecordsetCount` leaves out with `Trim(ContactType) = ''`.

Checked by running the program against a WAE SSB log of six contacts and a
series of ten received: the band rows read 4, 3, 2 and 2 and add up to the 11
in the total row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-09 20:25:45 +00:00
erikbrockoandClaude Opus 5 de8edafa0c Add the scoring review against om5m.s3db
The review of 6e24e00 against the decompiled reference and a replay of the
56,284 rows of om5m.s3db, with the per-instance comparison in the CSV.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-09 20:12:51 +00:00
erikbrockoandClaude Opus 5 a15bc8a6a2 Keep an excluded contact out of the score the way N1MM does
Three things were wrong with a contact the operator takes out of the claimed
score. The decompiled reference gives the rule in four places:

- `Contest.cs:437` returns zero points when `IsClaimedQso` is 0.
- `ContestInstance.cs:307` builds the where clause `CLAIMEDQSO = 1`, which the
  dupe search (`DBSupport.cs:1189`) and the multiplier search
  (`DBSupport.cs:1327`) both use, so an unclaimed contact takes nothing away
  from a later one.
- The score query (`DBSupport.cs:1807`) adds `ContactType <> 'D'`, which the
  other two do not. A contact marked as a dupe is therefore still searched for
  dupes and multipliers, and is left out of the score alone.
- `Contact.cs:1563`, `3307` and `3881` write `X-QSO:` rather than dropping the
  contact from the Cabrillo file.

`ContestLog` scored an unclaimed contact in full, and the Cabrillo writer left
it out of the file. `Qso.IsInScore` now holds the score rule and
`StatisticsPivot` uses it instead of its own copy.

The same reference also fixes the sent serial in three Cabrillo exporters.
ARRL 10M, Ukrainian DX and YO DX HF formatted the sent exchange from the
station rather than the contact, so a DX entrant exported `001` on every line.
N1MM reads the contact's own number: `CArrl10m.cs:122` writes it unpadded,
`CUkrainian.cs:232` and `CYOHFDX.cs:167` pad it to three digits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
2026-09-09 20:12:51 +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 6e24e00ec8 Copy the statistics table and graph to the clipboard
Copy All takes whichever tab is open, as N1MM's does: the table off the table
tab, a picture of the graph off the graph tab. Copy Table as Text and Copy
Graph as Image do either one from either tab.

`StatisticsTable` lays the pivot out as the window prints it — the headings,
the rows, the Total line — and pads it into columns for the text form, so the
grid, the clipboard and Print to File all say the same thing.

X11 asks for a picture when the other program pastes rather than when it is
copied, so the bitmap given to the clipboard is held until the clipboard is
given a different one. Disposing it at the end of the copy killed the program
on the next paste; the table pasted into a second Avalonia program reads back
lined up, and the picture reads back as a 916x438 bitmap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-04 07:59:22 +00:00
erikbrockoandClaude Opus 5 acd22a9b5e Count the log under any two of its fields
N1MM's statistics window. `StatisticsPivot` counts the contacts under one row
field and one column field, with the row total, the running total and the
column totals N1MM prints. The fields are N1MM's own two lists: 29 down the
rows, 20 of them across the columns, sorted the way N1MM's SQL sorts them, so
zone 10 comes after zone 2 and 80M before 20M.

The window has N1MM's two tabs. The table is the grid with `Tot` and `Accum`
and a `Total` line; the graph is a stacked column per row, one segment per
column, in one of N1MM's twelve palettes. "Include Dupes & X-QSOs" counts the
contacts left out of the claimed score and the ones marked as dupes.

Checked against a 3775-contact CQ WW SSB log: 3762 counted, which is what
`select count(*) ... where CLAIMEDQSO = 1 and ContactType <> 'D'` returns, and
the per-band totals match band by band.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-04 07:47:30 +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 0b92bfc6ee Fill in the rest of N1MM's network status window
Seven tabs now, in N1MM's order, and the messages behind them.

**Options** holds what there is something behind: Log QSOs at all Stations,
which decides whether the contacts the others log are kept here; the
message trace, moved off the Messages tab where N1MM does not keep it; and
Run1/Run2, which used to be guessed from the radio number and is now the
operator's choice, sent in every `STATUS` and every `XMIT`. The transmit
interlocks stay out: this program does not block a transmitter, and a
station that claimed it did would be waited on for nothing.

**Call Stack Targets.** A callsign heard at one station and handed to the
position that will work it — `STACKANOTHERCALL` with the call alone,
`STACKCALL` with the contact behind it. The target is picked from the tab
or from the station list, and what arrives goes on the call stack of the
radio the operator is on, sorted by whether it is a multiplier here.

**The master** is the station the entry follows for cluster spots and the
time. `MASTER` names it to everybody, so the column agrees everywhere.

**The clock.** `TIME` carries a station's clock and Force Time Sync sends
it. N1MM sets the clock from it when it is running as an administrator and
says how far out it is when it is not; this program always does the second,
because setting the clock needs root and a contest logger has no business
asking for it. Anything over two seconds is called out.

**Serial numbers.** Reset Serial Numbers at all Stations sends
`RESETQSONRS` and every station starts again at one. The contacts already
logged keep the numbers they were sent with.

**What the entry agrees on** is now checked rather than assumed:

| What | How |
|---|---|
| the contest | `CONTESTNAME` as soon as a connection opens, and again in every `CHECKSUM` |
| the country file | its issue date — the publisher's `=VER20260826` — rides in `STATUS` |
| the computer name | longer than N1MM's fifteen characters is named at startup; a second computer answering to this one's name is named when its beacon arrives |
| the station number | two stations on the same number are named when the second says so |

`CountryFile` reads its issue date, which nothing had asked it for before.

Left for later: the master relaying its cluster spots, and the shared
serial-number pool where stations claim numbers in advance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 23:15:44 +00:00
erikbrockoandClaude Opus 5 b5d86c1677 Send the messages N1MM actually reads
Three of the messages this program sends were its own invention or in the
wrong units, so an N1MM beside it read them wrongly or not at all.

| Message | N1MM | What was sent |
|---|---|---|
| `XMIT` | transmitting, the function key's caption, Run2, F1 pressed, stop sending | transmitting, the radio number |
| `PASSFREQ` | the frequency in hundredths of a kilohertz, and nothing else | the frequency in kilohertz, and a callsign |
| `FREQ` | the frequency in hundredths of a kilohertz | `BANDMAP`, which N1MM has no such message for |

So N1MM showed a radio number where the other operators read what a
station is sending, read a pass on 14205.5 as 140.25, and never saw this
station move at all. `BANDMAP` is gone: the frequency goes out in `FREQ`
when the radio moves and the mode, run and the rest ride in the `STATUS`
that follows it, which is where N1MM carries them.

The `Msg` column now shows what a station is sending, which is what the
caption is for. `PassCall` and `RadioNumber` are gone from the station:
neither is on the wire, so neither was ever anything but this program
talking to itself.

**A station that reconnects resynchronises by itself.** N1MM sends
`LASTQAT` — when I last worked anybody — as soon as a connection opens,
and a station holding nothing that late asks for the hours in between. An
empty log asks for nothing: a program that has just been started has no
business pulling the whole contest across, and N1MM suppresses it the same
way. The answer goes over the connection this station opens, which is not
always up when the message arrives, so a resync that has nothing to ask
over waits for it rather than being lost — and one that still cannot send
stops and says so instead of leaving the row resynchronising for the rest
of the contest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 22:54:16 +00:00
erikbrockoandClaude Opus 5 2a21c772ad Resynchronize a log with the other stations, and rescore it
A station that was away — a laptop that lost the network, one that was
switched off — comes back missing whatever the others logged while it was
gone. N1MM fills that in by walking the log an hour at a time, and this is
that walk, on the same messages:

| Message | What it says |
|---|---|
| `CHECKSUM` | here is what I hold for your contacts in this hour, and the contest and callsign I am logging |
| `DELETEQS` | drop your copies of my contacts in that hour, I am sending them again |
| `RESYNCQSO` | one of those contacts |
| `CONFIRMED` | here is what the hour hashes to at my end — which also asks for the next hour |

Each station is the authority on the contacts it made, so an hour that
disagrees is replaced rather than merged. The answer to one block asks for
the next, and the walk ends when the block starts after the end of the
period. The Resynchronize tab counts the hours that agreed, the hours that
were sent again and the hours that still disagreed, which are N1MM's
columns.

**`LogChecksum`** is N1MM's: a CRC32 over the `DXLOG` columns of the block,
ordered by time and then callsign, leaving out what each station works out
for itself — points, the three multiplier flags, the country prefix, the
continent, the WPX prefix and `IsOriginal`. Two stations may disagree about
those and still hold the same log. A checksum that disagrees for any other
reason costs one block of traffic, which is what a resync is for.

**The link still holds no log.** It asks one through `ContactLog` for the
contacts of a computer in a block of time, and `SessionContactLog` answers
on the window thread, because that is where the log is touched.

**Every contact logged here now carries the computer that made it.** N1MM
stamps `NetBiosName` and `NetworkedCompNr` on every row and this program
did not, so its own contacts belonged to no station and a resync could not
ask for them.

**Rescore** writes the points and multiplier flags of a period back to the
database. The log in memory scores itself whenever it changes; the stored
rows do not, and N1MM reads them rather than working them out. A contact
that arrives from another station after the one that took a multiplier
leaves those columns behind. A station can also ask the others to rescore
when its resync finishes, which is the flag on the end of `CHECKSUM`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 21:52:09 +00:00
erikbrockoandClaude Opus 5 68b29d722a Give the network status window N1MM's tabs and columns
The window was one flat table of columns this program made up. N1MM's has
tabs and a fixed set of columns, and an operator who knows one has to be
able to read the other.

**The window** now has Stations, Actions and Messages. Stations is a grid
with N1MM's headers in N1MM's order — Computer, IP Address, Pass, Run,
10, 100, Freq, Op, Msg, Send, Receive, Band, R/M, Mode — and a row per
station, this computer included. Right-click a row to pass the contact
being worked to that station, to talk, to show the connection log, or to
close and open its connection. Actions holds the talk line, the pass
frequency, the echo and the networking switch; Messages holds the chat,
the connection failures, the echo round trips and a trace of every
message that arrives.

**`STATUS`** is what fills those columns. N1MM sends it every few seconds
whether anything has changed or not — the pass frequency, the frequency,
run or search and pounce, the operator, the rates over the last ten and
the last hundred contacts, the mode, the categories and the version — so
a station sitting on one frequency for an hour still has a current row.
`BANDMAP` only says a station moved. The five transmit-interlock flags
are sent as false: this program does not block a transmitter.

**`Rates.PerHour`** reads the two rate columns out of the log. Count back
over the last N contacts, stop at a gap longer than the contest's minimum
off time, and treat a span under a minute as a minute, so three contacts
in five seconds read as 180 an hour rather than 2160.

A connection closed by hand stays closed. Without that flag the next
beacon opens it again within ten seconds, which makes the menu item look
broken.

Left out, because there is nothing behind them: N1MM's Resynchronize and
Rescore tabs, the transmit interlocks of its Options tab, the Master
column and the call-stacking target tab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 21:39:42 +00:00
erikbrockoandClaude Opus 5 6f2f39768c Show the other computers, and apply the ten-minute rule
Three pieces on top of the link:

**The network status window**, on the entry window's Window menu. A row
per station, this computer included the way N1MM shows it: number,
address, operator, band, mode, run, transmit, pass frequency, the last
message type, how long ago it arrived, the counts each way and the echo
round trip. A station that broadcast the wrong version is named under the
table in red — that is the one fault where everything looks connected
and nothing arrives. The box at the bottom sends a line of chat, and
Echo asks every station whether it is there.

**Where this station is** goes out once a second when it has changed.
The frequency, the mode and run have a dozen places they can change from
— the radio moving, a band button, a QSY typed into the callsign box —
so it is read and compared rather than announced from each of them.

**The ten-minute rule.** `BandChangeRules` already counted changes and
the stay on a band, and the entry window already showed the countdown,
but only a user-defined contest carried a rule, so every built-in
contest allowed anything. `ForCategory` gives a multi-operator entry
with one or two transmitters a ten-minute stay, which is N1MM's fallback
in `ContestInstance.BandChangeTimerDuration` for every contest that does
not name its own. Its per-contest table is a few hundred cases in a
decompiled hash switch and is not repeated.

Config ▸ Edit Networked-Computer Names now covers both networks: the
12060 broadcast to other programs and the 12070 link, with the station
number, the port, the version to claim and stations named by address for
a network where a broadcast does not reach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 15:26:42 +00:00
erikbrockoandClaude Opus 5 c0e1bc1e1b Talk to the other logging computers on N1MM's own port
N1MM has two networks and this program had only one of them. Port 12060
carries XML to other programs — a spot tool, a score poster — and
`StationNetwork` already writes that. Computer to computer, N1MM uses
port 12070 and a different protocol, which is what this adds.

The wire format, from `MultiOpManager` and `MultiStation`:

    DATA__07%SHACK-PC%QSO%2026-09-03 12:34:56%DL1ABC%…~__DATA

the sending station's number, its computer name, the message type and
the fields of that type. `%` and `~` cannot appear in a field, so N1MM
writes `!` in their place. `StationRecord` reads and writes that, and
holds what has arrived until a `~` says a message is whole: TCP hands
over half a message as often as two.

Discovery is a UDP broadcast to the same port, six fields wide.
**N1MM refuses a station whose version is not its own** — it says
"Software versions must match" and drops it — so the version this
program broadcasts is a setting rather than its own version, and a
station that says something else is kept in the list with `Refused` set
so the operator can see why nothing is arriving.

`StationLink` runs both sockets, opens a connection to every station it
hears, and reports what arrives. `QsoRecord` is the thirty-five fields
of a contact in N1MM's order, from `MultiOpManager.cs:1007`; points and
multiplier flags are read but the log works them out again from the
rules, so two stations cannot disagree about a score. `AddStation` names
a station by hand, for a network where a broadcast does not reach.

A station can also be named rather than heard, and one that is connected
may say nothing for a while, so `NetworkedStation` is heard-from as soon
as it exists. Left at nothing, the sweep that drops quiet stations read
every new station as quiet since the beginning of time and closed the
connection the moment it opened.

Tested as bytes and over loopback. Nothing has talked to a real N1MM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 15:19:03 +00:00
erikbrockoandClaude Opus 5 9f635550bd Read a QTC series out on RTTY
N1MM's Send All: the whole series as one message — the heading, every
line with the operator's spacing between them, then the ending, with
`{TX}` and `{RX}` around it so it keys the transmitter and drops it
again. The shape is N1MM's, from `QTCWindow.cs:3609`. A line is the three
fields joined with hyphens, which is how N1MM writes them, and Snd n
sends one line again for a station that missed it.

One message rather than a button per line because a series is close to a
minute of transmission at 45 baud: the type-ahead buffer holds it and
paces the engine.

Three settings with N1MM's names and defaults — `WAESendAllHeadingText`,
`WAESendAllEndingText` and `WAESQTCSpacing` — on the QTC window's setup
dialog. `{ENTERLF}` is new to the expander and stands for a carriage
return and a line feed; `{QTC}` is filled in by the window, which is the
only place that knows which series is going out.

The messages are tested as text. Nothing has gone on the air.

Left out: N1MM's four SSB recordings, which need a voice keyer, and its
RTTY messages for the station taking traffic down, which are typed into
the digital window's transmit pane here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 15:02:08 +00:00
erikbrockoandClaude Opus 5 0fc8d7e405 Write down what starts the transmit pane again
Two things do, not one: the engine reporting the drop, and a new message
being keyed. The second was missing from the text, which is the fault the
last commit fixed.

The claim that the bridge does not forward MMTTY's window message 32772
is dropped. `docs/digital-bridge.md` argues the control cannot raise
`OnTranslateMessage` for a message it has an event for, `OnPttEvent` does
report drops here, and the two files disagreed. Says what is known and
what is not instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 14:46:37 +00:00
erikbrockoandClaude Opus 5 f49a8c10fd Start the transmit pane again on every new message
The macro buttons went wrong at the end of a transmission, both faults in
DigitalEngineSender:

- The engine reports the transmitter drop on its own thread.
  WhenTransmitChanged took the state lock, decided the message had ended,
  released the lock, and only then called Buffer.Ended(), which clears
  what has gone to the engine. A macro pressed on the last character got
  through StartAsync in that gap and had already flushed its own text into
  Sent, so Ended() wiped the new text off the pane while the engine
  transmitted it. Ended() is now called inside the same lock.

- The pane only started again when the engine reported a drop. Two macros
  in a row keep the transmitter up, so that report never came and Sent
  grew with every press. Everything in Sent is locked, because it is in
  the engine and cannot be taken back, so the whole pane became
  read-only. TypeAhead.Started() drops the last message's sent text and
  keeps what was typed ahead, and StartAsync calls it whenever it keys a
  new transmission.

The rest of this commit is the digital transmit work these fixes sit on:
the pane as one coloured box, the sender's three keying states, the
type-ahead feeder paced by the clock with the engine's count as a brake,
{RX} flushing what is left in one piece, and the entry window's function
keys reading the digital macros on a digital mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
2026-09-03 14:40:30 +00:00
erikbrockoandClaude Opus 5 7ae60be4a0 Write down what the count lag and the symbol count measure
The third probe run puts numbers on both. TxBufLen lags by 100 to 150 ms, about
one character at 45.45 baud: twenty-one characters pushed at 2496 ms read 0 at
2497, 2547 and 2597, then 26 at 2647. It moves once per symbol transmitted, so
readings 50 ms apart repeat.

It read 26 for 21 characters because it counts Baudot symbols, and the message
carried two digits: a shift to figures and a shift back each time, plus one at
the start. That is the case the cap was needed for and I had not seen. A contest
exchange is mostly digits, so the engine always has more to transmit than the
clock thinks, and the clock alone would run ahead of it on every QSO.

Slack of three characters covers both, so nothing changes in the pump. The
count reaching 0 does not cut the end of a message off either: it emptied 350 ms
before the last character was decoded back and the engine held the transmitter
up for another 900 ms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtspmWmS7f8kUvcyaHpRWZ
2026-09-01 23:20:11 +00:00
erikbrockoandClaude Opus 5 5370ad3f6d Do not trust the engine's count as the pace
The second probe run read TxBufLen as 0 one millisecond and again fifty
milliseconds after twenty-one characters had been pushed, while those characters
were already going out: the first of them was decoded back off the air 420 ms
later. The number is right when it is a second old and wrong when it is fresh,
so it runs behind the engine.

A pump that fed on it would have handed over the whole message in half a second
and put all of it beyond reach, which is worse than the clock it replaced. So
the clock is the pace again, and the count is a check on it: the engine is never
given more than Lead + Slack characters however fast the clock says to feed, and
a message ends when the count and the clock estimate both say it has. Characters
fed since the last answer are added to it, so a stale answer cannot be spent
twice.

The engine is set to Character out: ABCD with no space after it went out at
once. Word out would have held it, so the holding case stays, but it now needs
the count to be neither going down nor being added to, since an engine kept
exactly at the cap has a count that does not move either.

The probe writes down every reading for two seconds after a push and does not
believe an empty one in that window, which is what the run needed to measure the
lag and did not do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtspmWmS7f8kUvcyaHpRWZ
2026-09-01 23:13:28 +00:00
erikbrockoandClaude Opus 5 4118cd6d33 Pace the pump on what MMTTY says it has left
The engine probe answered two questions on the air, and they point opposite
ways.

TxBufLen is the number of characters left to transmit. Seventeen characters
pushed, five decoded back, and it read 12, counting down to 0 as the message
went out. So the pump no longer counts character times off the clock: it asks
the engine, feeds while the answer is under Lead, and asks again. The baud rate
is now only the fallback for an engine that will not answer.

A backspace is not an edit. Pushed in as a character it made the count go up by
four and the text went out unchanged, so text the engine has been given cannot
be taken back, only aborted. EngineTypeAhead was built on the opposite belief
and is gone, along with the setting that chose it and the interface that existed
to switch between the two.

A count that stops going down means the engine is holding what it has: MMTTY set
to Word out keeps a word until the space after it. The pump feeds one character
per look while that lasts, so the space arrives and the word goes out rather
than the message sitting there.

The probe kept two bugs of its own that this run showed: it read the count a
millisecond after pushing, saw 0 and called the message finished, and it started
a step while the previous unkey was still in flight, so that step ran with the
engine down. It now needs two empty answers in a row and waits for the engine to
say it has stopped. It also asks what {RX} does to a word the engine is holding,
which decides whether a macro without a trailing space loses its last word.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtspmWmS7f8kUvcyaHpRWZ
2026-09-01 23:02:00 +00:00
erikbrockoandClaude Opus 5 917a05b898 Key MMTTY the way N1MM does, with the control's PTT property
Nothing was ever transmitted. SetMmttyPTT does not start a transmission: N1MM
calls it with 1 to stop once the buffer is empty, which is its XmitOff, and with
0 to stop now, which is its AbortXmit. A transmission starts by setting the
control's PTT property, in XmitOn.

So the bridge learns `key <0|1>` for that property, and MmttyEngine now keys
with it, ends a message with SetMmttyPTT(1) so the buffer still goes out, and
aborts with SetMmttyPTT(0). This is the digital {TX} in the entry window and the
digital window as well as the probe: none of them could key the engine before.

The probe checks that the engine keys before it measures anything, and stops
with a plain statement if it does not, rather than reporting numbers from an
engine sitting still. It also asks a new question: whether the engine holds a
word until the space after it, which is MMTTY's Way to send. Received characters
are marked as noise while the engine is not transmitting, since a machine with a
sound card decodes the band all the way through the run.

The first run on a real engine says MMTTY 1.70 connects, the control answers
TxBufLen and refuses NotAProperty with DISP_E_UNKNOWNNAME. What TxBufLen counts
is still open: read while nothing was transmitting it rose over time and rose by
four after four backspaces, which is not what characters-left would do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtspmWmS7f8kUvcyaHpRWZ
2026-09-01 22:51:43 +00:00
erikbrockoandClaude Opus 5 54b9181c06 Let MMTTY hold the text waiting to go out, behind a setting
MMTTY has a type-ahead buffer of its own: characters go into it, a backspace
takes back one it has not transmitted, and TxBufLen says how many are left. Used
that way it paces itself, so there is no gap between characters to tune and no
baud rate to keep in step with the engine.

EngineTypeAhead does that, and Config > Digital picks between it and the pump
that is there now. Off is still the default: three things it rests on have never
been seen with a real engine.

tools/Nonemm.EngineProbe asks the engine those three questions and writes the
answers to a file. It starts MMTTY through the bridge, pushes a message, polls
TxBufLen while it goes out, backspaces over text that has and has not been
transmitted, and logs what came back on the receive side and when.

The bridge learns one verb for it: `buffer` reads TxBufLen and answers with the
count, or -1 when the control will not say. A property the control does not know
is a log line rather than an error, since it stops nothing.

TypeAhead and EngineTypeAhead share the TransmitBuffer interface, which is what
the digital window now works through, so the window does not know which one it
has.

docs/unfinished.md states what each buffer assumes and how to run the probe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtspmWmS7f8kUvcyaHpRWZ
2026-09-01 22:41:18 +00:00
erikbrockoandClaude Opus 5 da7f7fb3f5 Keep the engine fed instead of feeding it a character at a time
The pump handed the engine one character and then waited a character time
before the next, on purpose, so it stayed behind the engine rather than ahead.
That leaves the engine with an empty buffer between every two characters of a
message, and an engine with an empty buffer transmits idle rather than waiting.
The idle is added to how long the message takes, which in a contest is time
paid for nothing.

The pump now keeps two characters in the engine: one being transmitted and one
behind it, so the engine never runs dry. The clock at the baud rate says when
the engine has room for the next one, and the engine reporting that it has
stopped transmitting sets the estimate back to an empty buffer.

The last two characters are now beyond reach rather than the last one. Everything
before them can still be rewritten, which is what the pane is for.

A message is finished when the buffer is empty and the engine is estimated to
have transmitted what it holds, so {END} and {RX} no longer run while the engine
still has the last characters of the message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtspmWmS7f8kUvcyaHpRWZ
2026-09-01 21:56:41 +00:00
erikbrockoandClaude Opus 5 9d0af06686 Say how the transmit pane works and what would make it exact
The pump is paced off the clock. XMMT.ocx has a TxBufLen property and answers
when it is asked, so the bridge could report what the engine still holds and
make the red text exact, but the engine does not start on this machine and the
property could not be watched while a message was going out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-09-01 05:25:49 +00:00
erikbrockoandClaude Opus 5 f69c630ba1 Show what has gone out in the transmit pane
The transmit pane is now the type-ahead buffer. What has gone to the engine is
in red beside the box and cannot be reached; what is still to go is in the box
and can be typed over, added to or deleted while the engine works through it.
A macro fills the pane rather than the engine, so the operator can correct a
call the message is still sending.

The cursor holds the pump: nothing behind it goes out, so an engine that
catches up with the operator idles instead of transmitting half a word. With
the box out of focus there is no cursor and everything goes.

Escape and the RX button drop what has not gone out and stop the engine where
it is. Enter puts in the carriage return the engine takes as a new line. The
pane starts empty for the next message.

The baud rate the pump counts by is in the digital setup window, beside the
alignment frequency. It has to match the engine: 45.45 unless the contest says
otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-09-01 05:25:49 +00:00
erikbrockoandClaude Opus 5 65f1051bc1 Hold a digital message back so it can still be changed
A digital engine takes a whole message and transmits it at 45.45 baud, which
takes several seconds. Once it has the message nothing can be changed, so an
operator who sees the wrong call going out has to stop the transmission and
start again.

TypeAhead keeps the message instead and feeds the engine one character at a
time. What has not gone out yet can be rewritten, added to or deleted. Cursor
is how much of it may go: the pump stops there and the engine idles on the air,
which is the diddle a RTTY engine sends between characters anyway.

The pump counts character times off the clock at the baud rate rather than
asking the engine what it has left, so it runs a little behind: a gap between
two characters is idle on the air and costs nothing, while feeding faster than
the engine transmits would put text out of reach again.

Every digital message goes through it — the function keys, ESM, the QTC window
and the digital window's own buttons — because they all send through
DigitalEngineSender. Finished is now raised when the buffer runs dry, which is
when the message has really gone, so what stands after {END} runs then and {RX}
drops the transmitter at the right moment. The transmitter dropping only counts
as the end when nothing is left to send: an engine that keys itself off what it
is given drops between two characters as well.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-09-01 05:25:36 +00:00
erikbrockoandClaude Opus 5 0e2aee545e Say what the digital window still does not have
Sending a text file is in, so it comes off the list. AutoTRXUpdate goes on it:
the title bar carries the offset frequency now, but the entry window and the
log still record the dial frequency.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-09-01 00:03:35 +00:00
erikbrockoandClaude Opus 5 f3908840b3 Lay the digital window out the way N1MM does
The window now carries what N1MM's does. Across the top: the TX indicator, the
Letters/Figs box showing the word under the pointer in the other Baudot case,
and the MouseOver box showing the word itself. Down the left of the receive
pane is the pause bar, green while text prints and yellow while it is held.
Under the panes are the message buttons and the row of engine controls, in
N1MM's order.

Both of N1MM's window scroll types are here. The scrolling pane adds lines at
the bottom; the non-scrolling pane keeps text where it was printed, underlines
the line being printed to and walks it down the window, so a call does not move
as the pointer reaches it.

A callsign is checked before it is coloured: anything shaped like a call under
the generic routine, only calls in the files that are ticked under the
resources routine, and under the combination everything, with a call the files
do not hold marked in yellow. The garbage text search finds a Master.scp call
inside a run of noise.

The mouse and the keys are N1MM's. Shift+click converts the case before the
word is transferred, Ctrl+click puts it in the box the cursor is in, Alt+click
puts a call on the stack, a double click replaces the call in the box. The
receive pane and the grab list have their right click menus. Alt+T and Ctrl+K
turn the transmitter on with the cursor in the transmit pane, Alt+G grabs, Esc
aborts.

New from the menus: hover mode, the callsign pause routines, sending a text
file, a date-stamped copy of the receive pane in ExportFiles, clearing the grab
list on a CQ or a QSY, the ham default and AFC off on a run to S&P change, AFC
on with CQ, and raising the window with the entry window it belongs to. The
grab list holds ten calls, as N1MM's does.

The setup window is N1MM's dialog: a General / MMTTY Setup tab, an Engine tab
for the Wine and bridge paths, and a Message Setup tab with the buttons laid
out as they sit in the window, a caption and text box for the one being edited,
the macro list, the count of buttons to show, and import and save of a .mc
file. Right-clicking a message button opens that tab, so the free-form macro
dialog is gone.

The title carries the dial frequency with the mark tone added to it, or the
dial frequency alone when the radio already reads out what it transmits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-09-01 00:03:30 +00:00
erikbrockoandClaude Opus 5 1da35e6c45 Keep the digital window's N1MM settings
The settings N1MM keeps for its digital interface: how many message buttons to
show, which of the two window scroll types to use, hover mode, the callsign
pause routines, what a callsign is checked against before it is highlighted,
which files that check reads, whether the grab list is cleared on a CQ or a
QSY, what happens to AFC and the mark tone on a run to S&P change, and whether
the receive pane is copied to a text file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-09-01 00:03:13 +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 f536b1b6a1 Read a word in the other Baudot case
RTTY sends one shift character to say whether the codes that follow are
letters or figures. When it is lost to noise the printer stays in the wrong
case for the rest of the line, so 599 prints as TOO, 73 as UE and CQ TEST as
:1 53'5.

Baudot.LettersFigs swaps the case of a word using the US teleprinter table,
which is the one N1MM uses in DigitalMMTYIo.Letters_Figs.

N1MM converts a letter to its figure but not a figure back to its letter, so
its box turns VE4AEO into ;3R-39 and cannot turn it back. The table here is
read both ways, which is what MMTTY's own right click does: a call printed in
the wrong case is the case an operator has to read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-09-01 00:03:01 +00:00
erikbrockoandClaude Opus 5 14209e01e8 Say why the folders cannot be made instead of dying
Something already sitting where Documents/N1MM Logger+ should go — a file, or
a link to somewhere that is not there — threw out of Directory.CreateDirectory
before the program had a window, so the operator got a stack trace and nothing
else.

CreateFolders answers with what stopped it, and the program shows it in a
window naming the path and stops there. NONEMM_HOME moves the whole lot
somewhere else, for a station that keeps its files off the Documents folder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 23:16:55 +00:00
erikbrockoandClaude Opus 5 d5b5233ab8 Drop the migration from the older folder
Nothing has shipped, so there is nothing to migrate: the program makes
Documents/N1MM Logger+ and starts there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 23:09:01 +00:00
erikbrockoandClaude Opus 5 d49c87f86b Move the files into N1MM's own folder
The root is Documents/N1MM Logger+ now, so a station that runs both programs
has one set of files rather than two: one Databases folder, one SupportFiles,
one Wav with a folder per operator in it. What N1MM has no place for goes in a
Nonemm folder inside it, which is where settings.json sits.

A system that does not say where Documents is — most Linux systems — gets
~/Documents/N1MM Logger+.

Files an older version kept in Documents\Nonemm or ~/.config/nonemm are copied
in the first time this version runs, and the log and call history paths in the
settings are moved with them, so the copy that opens is the copy that came
over. The older folder is left as it was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 23:07:03 +00:00
erikbrockoandClaude Opus 5 2c51b5293a Keep the files in N1MM's folders
The folders under the user's own directory are N1MM's now, with N1MM's names,
read out of N1MMLibrary.dll: Databases, SupportFiles, UserDefinedContests,
FunctionKeyMessages, CallHistoryFiles, ExportFiles, GoalFiles, LettersFiles,
QsoRecording, SkinsAndLayouts, SystemFiles, TransactionLogFiles, Diagnostics
and Wav. A station that runs both programs keeps its files in one shape.

An operator's recordings go in Wav/<his callsign>, which is where N1MM plays
them from, and the folder is made when he takes the radio. Cabrillo and ADIF
start in ExportFiles, call history files in CallHistoryFiles, and the .mc
function key files in FunctionKeyMessages.

N1MM's three *DDL folders and its Piper folders are left out: they hold the
migrations for N1MM's own admin databases and the program it speaks with.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 22:59:45 +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 53c15d4061 Draw the grey line map
Window > Grey Line opens the world with the night on it. The dark area is
where the sun is down, and the band along its edge is the twilight from the
horizon to six degrees below it, which is the grey line an operator watches
for on the low bands.

Grayline works out where the sun stands: the subsolar point, the elevation at
a place, and the latitude the terminator crosses a meridian at, which is what
the map fills the night between. It is the same question SunTimes answers from
the other end, and the two are checked against each other in the tests.

The coastlines are Natural Earth's 110m coastline, public domain, cut to a
tenth of a degree.

Our own station is drawn from the grid square, the station being called and
the spots on the band from the country file. The map follows the clock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 22:09:16 +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 76b57cee1d Carry out the call history section directives
A call history file is built from years of logs, so its section and
state columns carry old names, and the file says at the top what to do
about that. The four lines that say it were read and passed over.

All four are now carried out, in N1MM's order: !!MapOnSection!! stores
the old Ontario sections as ON, !!ValidateArrlSection!! drops a section
that is not one of N1MM's, !!GTA2GH_NT2TER!! stores GTA as GH and NT as
TER, and !!Validate50State!! keeps a state, turns a section into the
state it is in, or drops the value.

The section list is the one N1MM's call history reader validates
against, which is not the list its entry window offers: it holds the
retired GTA and NT, and leaves out ON. It sits beside the reader in
Nonemm.Core rather than with the contest lists, which are the entry
window's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 14:21:02 +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 de010fb981 Say that the country-dependent exchanges are written
docs/unfinished.md said REF, ARRL 10M, the Ukrainian DX contest and the
Russian DX contest were not written yet. All four are in
src/Nonemm.Contests/Rules, and the first three implement SkipsField for
the country-dependent exchange the sentence is about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 13:32:12 +00:00
erikbrockoandClaude Opus 5 6c2ee0f78f Act on the .udc session, off-time and band-change settings
Four settings were read by nothing.

MultipleSessions = 0000/30 runs the contest in half-hour sessions from
00:00 UTC. The session is part of the dupe key, so a station worked in
an earlier one may be worked again, and ResetMultsEverySession and
ResetSNEverySession start the multipliers and the serial numbers over.

DupeQSOMinutesAgo is the other way back to a station: after so many
minutes rather than in the next session. The log holds the time of the
last contact per dupe key instead of only the key, and the setting's
IgnoreBand and ThisMode decide the scope, because a file that uses it
turns DupeType off.

MinimumOffTime drives the time on and time off the score summary now
shows under the score, and beside them are the band changes made against
the allowance for a contest that limits them. N1MM's countdown for the
minutes you have to stay on a new band needs its info window and is not
here.

UdcFile.Flag now takes 1 and 0 as well as True and False, which is what
these settings are written as and what N1MM reads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 13:32:01 +00:00
erikbrockoandClaude Opus 5 e3fa979584 Read the .udc CallHist multiplier and BonusPoints2
MultSqlString = CallHist counts every station the call history file
lists, once per callsign, which is how N1MM dedups it. The file reaches
the scoring code through ContestLog.History, and a QsoContext now
carries it; without a file loaded nothing counts.

BonusPoints2 = +50, calls.txt reads the callsigns from the support-files
folder and adds 50 to the contact, *2 doubles it and a plain 50 scores
50 instead. The folder comes from the registry, which the app passes
when it reads the .udc files. N1MM's other form, where the file is
grids.txt and the bonus is looked up by grid square, is not read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 13:31:25 +00:00
erikbrockoandClaude Opus 5 1d3167cd04 Check a .udc section multiplier against the list the file names
MultWindowType names the list N1MM validates a received section against,
and every exchange that was not all digits counted as a multiplier here.
Four of N1MM's lists are held now -- the ARRL sections, the US states,
the Canadian provinces and the 165 OK/OM districts, which came out of
N1MM's own exe -- and a file naming one of them counts only the values
on it. A file naming any of the hundred-odd others keeps the old rule.

The 2023 OK/OM DX log now scores contact for contact as N1MM did. Two
contacts in the 2026 log still differ: the district code is stored with
a trailing space, which N1MM matches against its list untrimmed, so it
counts no multiplier where this counts one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 13:30:27 +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 12dab9644a Log the time the BARTG exchange went out
The BARTG RTTY contest sends the time with the serial number, and both
stations log the time the other one sent. The entry window now has the
report, serial and time boxes N1MM's has, and the time this station sent
is stamped into the MiscText column as the exchange message goes out, or
at log time when nothing sent it. The Cabrillo line writes that time and
falls back to the time of the contact when the column is empty, which is
what N1MM writes, in N1MM's columns.

The total score was points times every multiplier. N1MM's class works
out points times the countries and areas together, times the continents.
Per-contact points and multiplier flags do not change, so the station's
2022 log still scores contact for contact as N1MM did.

N1MM has a class each for the March HF RTTY contest and the September
sprint. This program has one contest holding the March rules under the
sprint's name, and docs/unfinished.md now says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 13:29:08 +00:00
erikbrockoandClaude Opus 5 a07b181ee1 Write the numbered Cabrillo lines a .udc file asks for
CabrilloFormat 2, 3, 4 and 5 name the NAQP, NA Sprint, Sweepstakes and
section-and-serial lines. A file asking for one of those got the default
line; it now gets the layout, ported from CabrilloString2 and
CabrilloString4 in N1MM's Contact.cs, in N1MM's columns.

The exchange the layouts split into columns is built from the boxes the
file defines, in the order it defines them. N1MM builds it from the
section box alone, which writes only half of a two-box exchange such as
NAQP's name and state.

Format 6, the ARRL RTTY Roundup line, is still not read. No published
.udc file asks for it, because the Roundup has a contest class of its
own.

ExchangeSlots.ValueOf moves to Nonemm.Contests as QsoExchange.ValueOf,
so the contest code can read a contact through its exchange boxes
without a second copy of that switch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 12:27:49 +00:00
erikbrockoandClaude Opus 5 cf5bdd6082 Lay out a Cabrillo line the way a .udc file asks for it
`CabrilloString` names a field and a column width in turn. The line it makes
follows N1MM: the operator's callsign in the first 13 columns, then every
column padded to its width with one space after it, and a value longer than its
column cut rather than pushed sideways. The cut is new for every contest, not
just the user-defined ones — the columns are the sponsor's, not ours.

`CabrilloVersion` is read too, so a contest whose sponsor still asks for 2.0
gets it in the header.

A contest that lays out its own line can put the transmitter column where it
wants it, or leave it out, so the writer no longer adds one at the end.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 10:38:39 +00:00
erikbrockoandClaude Opus 5 5ab97251e1 Say what happens instead of making files do things
A file is written to; it does not write, score or name anything. Five comments
and doc lines said otherwise, two of them older than today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 10:13:25 +00:00
erikbrockoandClaude Opus 5 38e9010802 Let the published .udc files score the contests they are written for
I wrote SA 10 and OK/OM DX SSB in code after two failed attempts at the
download. The download works — the page posts a nonce with the form — and the
files say I got both contests wrong.

SA 10 is deleted from the code. Its file scores the station's log to the
contact, where the code had the wrong Cabrillo name and counted its multipliers
once in the contest rather than once per mode.

OK/OM DX now covers the CW running only, which is what N1MM's class is for and
what N1MM's own documentation says: the SSB running has different rules and a
pair of .udc files, one for each side. The CW rules go back to the class, so
Czechia and Slovakia are one side of the contest again. With the SSB files in
place both of the station's SSB logs score to the contact.

Reading those files turned up four things in the .udc reader: a file whose
extension is upper case was passed over, `Country` was not read as a
multiplier source, `OtherCountry` was not a points condition, and a section
multiplier counted the serial numbers the other side of a contest sends.

All Asian stays in code — N1MM has a class for it after all — with the Cabrillo
name and the band table it uses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 10:00:21 +00:00
erikbrockoandClaude Opus 5 264036f0f2 Add the twelve contests left in the station's log
REF, the Ukrainian DX contest, the BARTG Sprint, ARRL 10M, Russian DX RTTY,
RDAC, YO DX HF, Oceania DX, IARU Region 1 Field Day, SARTG RTTY, All Asian and
SA 10. Every contest name in the log now opens, and every one of these but
Field Day scores its log contact for contact as N1MM did.

Most are written from N1MM's own class and checked against the log. Three
things the log settled that the class does not say plainly: ARRL 10M counts its
multipliers once per mode rather than once, the BARTG Sprint counts a continent
once in the contest while its countries and call areas count per band, and RDAC
counts nothing but the districts and scores nothing for a station that sends
none.

All Asian and SA 10 have no class in N1MM, which logs them from a .udc file, so
those two are written from the published rules and the log. docs/unfinished.md
lists what is still not settled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 09:43:03 +00:00
erikbrockoandClaude Opus 5 7d9bc05e9a Add IOTA, the EU DX Contest, CQ 160 and Mexico RTTY
Four more of the contests in the station's log. IOTA, CQ 160 and Mexico RTTY
score every contact in it exactly as N1MM did.

IOTA pays 15 points for a station that sends an island reference and 2 for one
that does not, and counts each reference once per band and mode. CQ 160 is one
band, so its states and countries count once each. Mexico RTTY and CQ 160 both
take a state from one side of the contest and a serial or a zone from the
other, in the columns N1MM keeps them in.

The EU DX Contest is written from N1MM's class, including its list of what
counts as Europe: the European Union, its outermost regions and its overseas
territories, and nothing else. The station's own logs were made with the .udc
file, which scores and counts two things differently; docs/unfinished.md says
which.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 09:33:29 +00:00
erikbrockoandClaude Opus 5 3932f83147 Add the YOTA and OK/OM DX contests
Both are in the station's log and neither could be opened. The rules are
written from the published ones and checked by replaying the log.

YOTA scores a contact by the age the other operator sends: 13 points down to 10
for anyone 25 or under, whatever the continent, and 1 point or 3 for everyone
else. Every age worked on a band is a multiplier. The 2024, 2025 and 2026 logs
come out to the contact.

OK/OM DX is the first contest whose exchange differs by the other station's
country: a county from OK and OM stations, a serial number from everyone else,
in the two columns N1MM keeps them in, and `SkipsField` walks the entry window
past the box that does not apply. Points depend on which side the operator is
on, and Czechia and Slovakia count as the two countries they are. The 2023 log
comes out to the contact.

The Cabrillo exchange now gets the entry, because YOTA sends an age that only
the contest setup knows.

Two differences are left in the log, both from the .udc files the operator
used: their YOTA file only lists ages 7 to 25, so a younger operator scored as
an adult, and one OK/OM log was made with the file for stations outside OK and
OM while signing an OM call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 09:16:44 +00:00
erikbrockoandClaude Opus 5 5c0ca558c1 Score the last three cases the way N1MM scores them
The three differences left after replaying the log were ours on purpose. A log
has to score the same in both programs, so they now follow N1MM:

- ARRL DX and CQ WW RTTY count the 14 Canadian areas N1MM lists as `48SDC14P`,
  which keeps Newfoundland apart from Labrador. A station sending the postal
  code is counted as the area the contest names.
- A maritime mobile is scored from the call it signs. CQ WW says it counts for
  the zone alone; N1MM gives it the country, and so do we.
- A callsign the country file cannot place counts as a multiplier with an empty
  value, so every unplaceable call worked on a band shares one multiplier.

29 of the 37 contest instances in the log now agree with N1MM on every contact
and on the totals. What is left is the country file of the day and rows whose
cached points N1MM never worked out again after a call was corrected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 08:36:44 +00:00
erikbrockoandClaude Opus 5 04568d0ca3 Read the rest of what a .udc file says, and score what N1MM scores
Two pieces of work on the scoring engine.

The .udc reader now takes N1MM's published scoring vocabulary: the full
PointsPerContact condition set, the PointsMultBy family, PowerMult, BonusPoints,
the multiplier sources and the settings that say which stations bring a
multiplier in. MultMult is a weight rather than a switch, as in N1MM's
ComputeScore. The entry categories and the sent exchange reach the rules in a
new ContestEntry, so a contest can score by the power category it is entered in
or by what this station sends.

Then every contest in a real N1MM log was scored again from these rules and
compared with the points and multiplier flags N1MM wrote. That found five bugs:
ARRL DX and IARU read the exchange from the wrong column, a stored " " was read
as a value rather than as empty, CQ WW RTTY's own Canadian area names were not
counted, a maritime mobile scored nothing, and a contact in a mode the contest
does not run scored as if it were in the contest.

docs/n1mm-interop.md has the check and what still differs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-31 08:33:40 +00:00
erikbrockoandClaude Opus 5 9424c488ea Take the rest of what N1MM's QTC window does on CW
Reading the source again after this was tried turned up several things it had
wrong or left out.

The TU message went out only when reading a series out. N1MM sends it whichever
way the traffic went, and lets it name the series: {QTC} in it stands for the
header, so TU {QTC} 73 goes out as TU QTC 3/10 73.

The RX Ready button was hidden while sending and sent nothing on CW. It is
N1MM's QRV button: reading out it says R U QRV and keys QRV?, taking down it
says QRV and keys QRV.

The fields were asked for again with shift and Enter, which was made up. N1MM
uses the number keys, and writes them under the lines: 1, 2, 3 and 4 send the
time, call, serial or header of the last line again while reading out, and
shift with 1, 2 or 3 asks for that field while taking down.

The buttons now carry N1MM's names for the direction the traffic is going —
R U QRV, Snd Hdr, Snd1 to Snd10, Exit reading out; QRV, Hdr Agn, Agn1 to Agn10,
Exit taking down — and the Cfm column and Clear are hidden while reading out,
as N1MM hides them. The separate Send Hdr button is gone: N1MM has one button
there that sends the header one way and asks for it the other.

The log window gained N1MM's QTC? column for a contest that carries QTC
traffic. It holds the series, so a QTC row can be told from a contact; before
this a QTC row read as an ordinary one with odd values in the report columns.

Driven under Xvfb against a fake cwdaemon. Reading out: QRV?, QTC 3/2,
2319 DL7GGG 03, then 2319 and QTC 3/2 from the number keys, and
TU QTC 3/2 73 on Exit. Taking down: QRV, AGN TIME, AGN CALL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-30 23:23:22 +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 98d51ab7f5 Take a contact from another station without reopening the contest
AppSession.TakeFromNetwork wrote the contact to the store and then called
OpenContest to see it, which built a new ContestSession, read the whole log
back and built new entry positions. Anything the operator was typing at either
radio went with them, and the entry boxes were rebuilt under the other
operator's hands. It also ran on the socket thread, so it was changing the log
while the windows were reading it.

ContestSession now applies the three messages itself: AddFromNetwork,
ReplaceFromNetwork and DeleteFromNetwork write the store, change the log in
place and raise Changed. None of them raises Logged, Edited or Deleted, so
nothing goes back out to the other stations. Points and multipliers are still
worked out here from the rules rather than trusted.

A contact that arrives goes in where its timestamp says it belongs, through
the new ContestLog.Insert, and the log is scored again: of two stations that
worked the same multiplier, the one that worked it first keeps it, however
late the message turns up. Add would have appended it at the end and given the
multiplier to the wrong contact.

The socket thread now hands the update to the thread the windows run on, which
is where every other change to the log is made.

The matching rule moved with the code, so ContestSession answers what a
message from N1MM refers to: the contact id, then the call and time it had
before the edit.

Still not tested against a second station. Two of these cannot run on one host
to try it, because they share one UDP port.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-30 22:40:33 +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 a31c40fb2d List the earlier contacts with the call being typed under the log
N1MM's log window has two panes. The lower one lists every contact already
logged with the call in the callsign box, on any band and in any mode, so an
apparent dupe can be read off the screen rather than looked for.

EarlierContacts.Matching holds the rule, so it is unit-tested without a
window: three characters before anything is listed, a call that holds what is
typed, `*` for any run of characters and `?` for one, the grid square instead
when nothing is typed, ordered by call, mode, band and time, and fifty at
most. That is N1MM's DupeSQL, except that N1MM passes `?` to SQLite as a plain
character, which finds nothing.

The two grids share their columns, because a value in the pane has to sit
under the column it belongs to. Each column is now as wide as the longest
value it can hold, measured in the font it is drawn in, and the last column
takes what is left over. The values and the header are in different fonts, so
each is measured on its own. This also settles the column widths, which used
to change as rows scrolled into view.

A DataGrid does not pass its own font down to its cells, so the cell font is
set in a style. Without that the cells draw larger than the width measured for
them and every column is a character short.

Looked at under Xvfb with a call typed: the pane lists the two contacts that
hold it, under columns that line up with the log above.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
2026-08-30 21:00:04 +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 d347538fbc Lay the station form out the way N1MM lays it out
The dialog now carries N1MM's fields in N1MM's order, with the labels to
the left of the boxes and the tip text at the top right. New fields in
StoredStation: the address, the licence class, the position, the radio,
the antenna and the email address.

The grid square and the position fill each other in, as they do in N1MM.
Which box has focus decides the direction, because TextChanged arrives
too late to tell a typed edit from a programmed one.

The fields N1MM's form does not have are gone from it. Continent and the
country prefix are looked up from the callsign after Ok; the rest keep
the value they had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 11:32:22 +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 ac63987e0c Move and batch spots the way N1MM does
Two differences from N1MM in the spot path, both found by reading Packet.cs.

Randomising moved every CW spot. PacketSpot.Randomize moves a spot only when it
is not a dupe, not a self spot, not on a CQ frequency, is on CW and is not
working split, and Packet skips it altogether for a station passed to another
band. Moving a dupe or a split station makes it harder to find, not easier, so
this now leaves the same ones alone. Whether a station has been worked comes
from the log, so AppSession asks it before moving a spot.

Split needed knowing where a station is listening, which is in the comment. Qsx
reads it by N1MM's rules: QSX, UP, DOWN, DN, U or D, the word standing alone or
after a space, a bare number after QSX meaning kilohertz inside the band and
anything else an offset from the spot, an offset over a hundred kilohertz
refused unless it says QSX, DN70 read as a grid square rather than five down,
and a listening frequency outside the band thrown away.

Spots reached the bandmap one at a time, each one redrawing every window that
watches it. N1MM queues them and flushes the queue once a second, which is what
this does now: the filters and the randomiser run over the batch, and the
bandmap takes it in one call and raises one change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 10:15:40 +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 2e81191ea0 Colour the message editor, and let Escape close it
The lines are coloured the way N1MM colours them: grey for a comment, blue for
the keys while running, green for the keys while searching. Anything past the
twenty-four lines that are read is red, so a file with one line too many says so
where the operator is looking rather than quietly losing a message. The colours
are the ones the rest of the program already uses for a verdict.

Avalonia's text box paints one colour, so the colours are painted on a stack of
text blocks, one per line, with the box on top holding transparent text. Both
use the same font and the same padding, so they line up, and they share one
scroll viewer, so they scroll as one.

Three things had to be true for that to work, and each of them cost a run to
find. The lines are rewritten in place rather than rebuilt, because a control
that has been shown once cannot be handed back in a fresh list. The box's own
background is forced transparent in every state through its template, not just
through the property. And the coloured lines sit above the box: a focused text
box paints over what is behind it, which is why the colours were there until the
operator clicked into the text.

Escape closes the editor and keeps what was there before, which is what Escape
does everywhere else in the program.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:28:36 +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