A phone function key names a wav file instead of words, which is what N1MM's
own SSB file holds: `F1 CQ,{WAVDIR}\Cq.wav`. `{WAVDIR}` is the folder named
after whoever is at the microphone, a name is taken relative to `Wav`, and
commas play several files in a row. An SSB `.mc` file written for N1MM plays
here.
Window ▸ Voice Keyer lists every recording the messages name, says which of
them are there for the operator picked at the top, plays one, and records over
one. The recording is written beside the wanted file and moved over it at the
end, so a recorder that will not start takes nothing away.
`CommandSoundCard` runs `aplay` and `arecord`, and both commands are settings:
the audio path belongs to whatever the station runs, and a library would pick
one of them for the operator. A recorder is stopped with an interrupt so it
closes its wav file, and killed only if it ignores that. PTT is cwdaemon's
`<ESC>a` for as long as the recording plays, off for a station on VOX.
Two things follow. The QTC window sends on SSB: N1MM's four recordings on the
Ready, Agn, Cfm and Close buttons, with the lines still read out by the
operator. And alternating CQ duels on phone, because the voice keyer says when
a recording has finished — which it had to, since the CQ message is picked by
the radio's mode and would otherwise have been keyed as Morse.
Nothing here has been played on a radio: this machine has no sound card, so
what is tested is the program being run, the file filled in, the exit code and
the interrupt, with shell commands standing in for the two.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
The score window and the stored Contest.Multiplier1Name row both said
"Prefixes" for an Asian entrant, which counts DXCC entities.
Contest.MultiplierNames is now MultiplierNamesFor(StationInfo me), the
same shape as ExchangeFieldsFor. Every other contest returns the same
list it did before; All Asian returns "Countries" from Asia and
"Prefixes" from everywhere else. ContestDefinitions.For takes the
station as well, because that name is written to the log for N1MM.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
A box the walk between the entry boxes steps over cannot be typed in, so it
must not hold the contact back either. `EntryFields.IsComplete` counted every
box marked required, while `MoveFocus` stepped over the ones the contest does
not ask this station for, so a contest with a box for some stations only
could lock the operator out of his own log.
The seven contests the scoring audit named do not do that any more —
`bae40bb` gave each of them the boxes N1MM shows, and fifteen reproductions
across all seven and both station types log cleanly — but the rule they broke
was still there to break again. Completeness now takes the same
applicability rule the cursor uses, and `OperatingPosition` answers it in one
place for the entry window, the ESM window and logging alike.
The audit is marked up for the three findings that are already fixed and for
item 14, which turns out to be what N1MM does rather than a fault.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
The QTC window put nothing on the air on RTTY except the lines themselves,
so the operator typed the rest into the digital window's transmit pane and
copied the lines by hand. N1MM sends a message for each step and fills the
lines in from a click. Both are here now, with N1MM's messages, N1MM's
defaults and N1MM's settings behind them.
The messages, all of them settings in the QTC window's setup area:
- RX Ready, `WAERXReadyText`, `{ENTERLF}I AM QRV BK`
- All Agn, `WAEAllAgnText`, `{ENTERLF}ALL AGN ALL AGN BK`
- Agn n, `WAEAGNText`, `{ENTERLF}AGN QTC: $ $ $ `, where `$` is the line
- Save, `WAESaveQTCText`, `{ENTERLF}R R R ALL OK BK`
- R U QRV, `WAERUQRVText`, `{ENTERLF}R U QRV BK`
- Save, reading traffic out, `WAESSaveQTCText`, `{ENTERLF}GL DE {MYCALL}`
Each keys the transmitter, sends the text and drops it again, and an empty
one sends nothing. The line spacing default is N1MM's `{ENTERLF}` rather
than `{ENTER}`, which was wrong.
The buttons: R U QRV and Snd Hdr were hidden while reading traffic out on
RTTY, though that is where N1MM starts — R U QRV is the first button
pressed. All Agn is new, and so is the S button on each line, which strikes
a line through a QTC that arrived too broken to read so the rows after it
keep their places; Agn n clears the strike and asks for that line again.
Clicking a line in the digital window fills the next row, which is how the
series is copied on RTTY: the other station sends the header and all ten
lines in one transmission. A click anywhere in a line hands the whole line
to the QTC window, which reads the time, the callsign and the serial number
out of it by shape and flashes the line to show it was taken. A line that is
not a QTC is left to mean what a click always means there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
N1MM writes a contact type on every contact it logs: `D` for a station
already worked, `N` for one the entry may not work at all, and empty for an
ordinary contact. Nothing here wrote the column, so a dupe counted as a
contact worth no points instead of dropping out of the score, and the two
places that read the column did nothing: the QTC pile offered dupes that
N1MM leaves out, and the statistics window could not leave dupes out either.
The marking is in one place, `ContestLog.ApplyVerdict`, and every contest is
marked the same way. What changes between contests is what counts as a dupe
and which stations are workable, and both come from the contest, as they do
in N1MM's `Contact.IsDupe`. No contest module in N1MM marks differently. Our
own callsign is not workable either, which is what N1MM adds on top of the
contest rule.
Two details follow N1MM as well: QTC traffic keeps an empty column, because
its `IsDupe` only marks a row while the program is logging contacts; and a
mark that came with the contact stands, because N1MM marks a contact when it
logs or edits one and never clears marks in a pass over the log. Editing a
contact clears the column so it is worked out again, which is what N1MM does
at the same point: an edit can move a contact to a band where it is no
longer a dupe.
A dupe is now out of the score altogether — contacts, points and multipliers
— which is N1MM's `ScoreClass.AddContact`. The QSO count still counts WAE
QTC rows as contacts; that is the other half of the scoring audit's item 14.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Ctrl+Z opens the QTC window for the station being worked, which is the key N1MM
puts it on, and the window is laid out the way N1MM lays it out: the header,
ten lines of time, callsign and serial number, an Agn and a Cfm button beside
each, Clear, Close and Cancel underneath, and the same colours — green for a
line that will be saved, red for one still empty, yellow for one that cannot be
read. Enter and Tab move forward and space moves within a line, so a line can be
walked while listening to it.
Sending, the lines are filled from the log and cannot be edited: the contacts
not reported to anybody yet, oldest first, never the station being worked, and
never past the ten any station may have. The rule for what has been reported is
N1MM's — a contact counts as reported when a QTC row carries its call and the
serial we sent it — so the pair is kept in the row the way N1MM keeps it.
Which way traffic goes is not the operator's choice on CW and SSB: Europe
receives and the rest of the world sends. On RTTY it goes both ways, and there
the window carries a switch. That is the one piece of the workflow that is ours
rather than N1MM's, which toggles a QTC mode outside the window instead.
Nothing here transmits. N1MM sends the lines on CW and RTTY and plays recorded
voice messages on SSB; we have no QTC message templates, no voice keyer and no
digital window, so Agn, Cfm and RX Ready move the cursor and the operator sends
by hand.
Running it caught what the tests could not: focus set in the constructor does
not stick, so the first thing typed into a freshly opened window was dropped and
every field after it took the value of the one before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ctrl+B calls CQ on one radio, and when that message has gone out, moves to the
other and calls there. The keyboard, the entry window and the SO2R box follow
each turn. N1MM calls this dueling CQs and puts it on the same key.
The turn is taken when the keyer says the message has ended, not when a timer
guesses it has. So MessageSender grew a Finished event and a ReportsCompletion
flag, and both keyers fill them in: cwdaemon answers the <ESC>h reply request
that now goes out in front of every message, and a WinKeyer clears the busy bit
in the status bytes it sends of its own accord. The status-byte reading is in
WinkeyerStatus, away from the serial port, because that is the half that can be
tested without a keyer on the desk. A keyer that reports nothing refuses to
start alternating CQ rather than keying the second radio over the first.
AlternatingCq itself takes the keyer, a callback that calls CQ on a radio, the
gap and a wait function, so the alternation is tested without sleeping. The gap
is in Config ▸ Keyer and messages and will not go below 100 ms, which is N1MM's
floor too: an SO2R box works relays.
docs/keying.md writes down why cwdaemon does the timing and we do not. N1MM
keys DTR itself with a coarse sleep, a busy-wait and a margin that grows every
time the sleep overshoots, and it raises the thread to TIME_CRITICAL for the
length of the message. The busy-wait ports to Linux; the priority does not,
without CAP_SYS_NICE, and a garbage collection mid-element is audible. A direct
serial keyer stays a reasonable third option, to be taken knowingly.
Running it against a fake daemon that takes 1.5 seconds to play a message: six
CQs went out back to back and the keyboard moved between the two entry windows
each time. Escape stopped it, let the message in flight finish, and started
nothing further.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Available Mults and Qs window: a column per band, the multipliers at the top
of each column and then the stations that are only points. Clicking one puts the
radio there with the call in the entry window, the same as the bandmap does.
Every spot on the bandmap goes through ContestLog.Judge, and what comes back a
dupe is dropped, so the colours and the ordering agree with every other window.
A spot holds a callsign and a frequency and nothing else. For CQ WW that gives
the country multiplier, which is derived from the call, but not the zone, which
is what the other station sends. Where a call history file has an entry for the
call its exchange is filled into the candidate first, so a published CQZone
column brings the zone in and a Sect column makes Sweepstakes work at all.
Filling the zone from the country file instead was rejected: it is wrong for
every large country, and a multiplier that is not there is better than one that
is not real.
A spot has no mode either, so the candidate is judged in the mode the radio is
in. In a mixed-mode contest that means the answer follows the operator.
RadioPosition kept two private mappings — exchange value into a contact, and
call history field into an exchange box — and both are needed here, so they move
to ExchangeSlots and both callers share them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
N1MM's format, so a file published for a contest is read as it is: # comments,
!!Order!! naming the columns of the lines after it, semicolons in preference to
commas so user text can hold a comma, and a default column order for a file
that names none. The directives that change what is stored are acted on;
!!Order!! can appear again part way through, because these files are usually
several files stuck together.
Leaving the callsign box fills what the file knows into the exchange boxes that
are still empty. A box with something in it is left alone: what the station
actually sends beats what somebody published months ago. The zone comes from
the CqZone or the ITUZone column depending on which the contest asks for, and a
file with no Sect falls back to State.
The check window's two missing columns are in. History lists matching calls
from the file. Exchange is not about callsigns at all: it offers the values the
exchange box with the cursor in it can hold, which is what N1MM's exchange pane
does — it searches the contest's list of valid exchanges, not the log.
CheckCandidate.Call is now Text, because an exchange value is not a call.
The section-validating directives are read and passed over rather than acted
on. Acting on them means holding N1MM's section lists and its rules about
retired sections, and getting that wrong throws away good data.
Running it caught what the unit tests could not: the exchange was filled and
scored but the boxes on screen still looked empty, because nothing resynced
them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LoggingSession mixed two things: the contest, which one station has one of,
and what the operator is typing, which belongs to a radio. It is now
ContestSession and RadioPosition. Two positions share one log, one score and
one run of serial numbers, so a station worked on radio 1 is a dupe on radio 2.
Frequency, mode, split, what is typed and whether you are running belong to
each radio on its own.
A second radio opens a second entry window. It has no menu of its own: it is
another view of the same contest, not another program. Only the window for
radio 1 opens the database and the connections.
Ctrl+Tab moves the operator to the other radio and the keyboard follows.
Ctrl+Shift+Tab puts both radios in the headphones.
OtrspBox speaks OTRSP to an SO2R box over a serial port: TX1/TX2 for the key,
RX1/RX2 and RX1S for the headphones, AUXnnn for an output line. It follows the
active radio, and every message points the box at this radio before keying, so
a message cannot go out of the radio the operator has just left. A command that
would change nothing is not sent, because the box works relays; N1MM does the
same. The box takes a Stream, so what it sends is tested without a serial port.
Still missing: alternating CQ, and voice keying on the second radio.
Looked at under Xvfb with two fake radios, one of them split: both windows up,
radio 1 showing 14008.00 into 14020.0, radio 2 on 14030.00, Ctrl+Tab moving the
keyboard between them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>