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 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>
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>
N1MM keeps them on a Setup tab inside the window; here they are a dialog behind
a right-click, which suits a window this size better. Four of them, with N1MM's
own defaults: how many QTCs a series carries, whether the RX Ready and Cfm steps
are stopped at on the way through, and whether Hdr Agn and Agn clear what they
are asking for again.
Each one changes what the window does. The series count is passed to the search
for contacts worth reporting, the two skip settings are the focus order, and the
two clear settings are what the Agn buttons do. The Cfm buttons also disappear
when a RTTY station is reading its own log out, which is what N1MM does with
them.
N1MM's remaining QTC settings are all about putting traffic on the air — the CW
messages for Agn and TU, the field spacing, the four SSB recordings, the RTTY
templates — and nothing here transmits, so they are left out rather than shown
as settings that do nothing. The dialog says so where the operator will look.
The right-click menu needed a background on the panel to come up anywhere but
over a control: a panel with no background is not hit-tested.
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>
WAE is two contests in one. The QSOs are scored like any other, except that a
multiplier is worth four on 80 metres, three on 40 and two on the high bands, so
Multiplier grew a weight and ScoreTally sums weights rather than counting.
Europe works the rest of the world and the rest of the world works Europe; on
RTTY that rule is dropped and everybody works everybody.
The other half is QTC traffic: one station reads back contacts it has already
made, ten to a series, and every line is a point for both stations. Those lines
live in the log as rows of their own, exactly where N1MM puts them — Exchange1
says SQTC or RQTC, the reported contact's time, call and number go in the sent
report, received report and sent number columns, and the series in misc text.
So a log written by either program opens in the other with its traffic intact.
A QTC row is not a worked station: Contest.IsContact says so, and ContestLog
keeps such rows out of the dupe index and the multiplier index while still
counting their points. Cabrillo writes them as QTC: records.
Checked against eight real WAE logs rather than against my own reading of the
rules. Points agree with N1MM on all eight, to the contact. Weighted
multipliers agree on the six logs from 2022 on; the two older ones differ only
where the country file has since changed its mind, and in one place on purpose:
N1MM counts a callsign it cannot place as a multiplier with an empty value.
That comparison turned up two bugs of ours. A KG4 call is Guantanamo Bay only
when it is three or five characters long — KG4NE is, KG4IGC is an ordinary US
call — and the country file cannot say so, so every program that reads it
carries the rule. And Callsign is a record holding a list of modifiers, so the
generated equality compared that list by reference and two parses of one call
came out different.
Entering QTCs while operating is not there yet; the window is next.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CQWWRTTY resolved to the built-in CQ WW with the mode set to digital, and then
scored it as though it were the CW running: a contact inside your own country
worth nothing, two multiplier types, and CQ-WW-SSB in the Cabrillo header. Every
one of those is wrong for RTTY, and wrong quietly, which is the worst way.
RTTY is a different contest wearing the same name. Every contact scores — 1
inside your own country, 2 on your own continent, 3 off it. The exchange carries
the state or province after the zone, and that is a third multiplier per band.
A station outside the US and Canada sends DX there, so the box is not required
and DX brings no multiplier.
The state box being optional is deliberate: most of the log is DX stations, and
holding up an entry for a box that will hold DX costs more than it is worth.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The box names in EntryWindowInfo are N1MM's LogItem.LogItemNameType members.
We read four of them under names that are not in that enum, so a file naming
its section box SectionText got no section box at all, and a zone or a power
box was dropped whatever it was called.
SectionText, CQZoneText and PowerText are now read. SectText stays as well: it
is not one of N1MM's names but files in the wild use it.
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>
Behind the bandmap scale, the CW part of the band is one shade and the phone
part another, with a third for a digital block. That is what N1MM's graphical
bandmap does: ResizeBandPanels puts a CW, a Digi and an SSB panel behind the
scale, sized from its CWBands, DigiBands and SSBBands tables. Anything past a
band edge is shaded red, which is the only place the edges themselves show, and
it only appears now that the slice can reach past an edge.
The defaults are N1MM's: CW ends at 1840, 3600, 7125, 14150 and so on, and the
digital pair is empty until somebody fills it in, exactly as N1MM ships. They
are one worldwide set rather than one per ITU region, which is also N1MM's
answer — the reason recorded for leaving this out, that the segments differ by
region, was wrong about what N1MM holds. 60M and everything above 2M get no
entry, because N1MM's own numbers there contradict themselves: its 1.25M CW top
is below its phone start, and its 70CM CW top is above the band.
Config ▸ Sub bands edits them, one row per band in kilohertz, with a Defaults
button. Only the rows that differ are written to the settings file, so a plan
nobody has touched leaves nothing behind. BandPlan is an object rather than a
static table, so the stored changes layer over the defaults and the bandmap
picks up a save without a restart.
The numbers are a drawing aid, not a licence. Nothing stops a contact being
logged either side of a boundary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contest operating goes outside the band edges. Someone answers a CQ at 14352, or
a radio reports a frequency a few hundred hertz off, and Bands.ForFrequency
returned null: the contact scored nothing, missed the per-band dupe check and
exported with no band.
N1MM buckets a frequency by its whole megahertz — its BandFor divides the
kilohertz by 1000 and switches on the result, with 1 and 2 both 160M, 3 and 4
both 80M, 28 and 29 both 10M — and never looks at a band edge. Same rule here,
except that the real edges are checked first: 2190M and 630M are both under
1 MHz and share the bucket, so nothing else can tell them apart. A frequency
under 1 MHz that misses both edge ranges still has no band, as it does in N1MM.
The bandmap clamped its slice to the band edges, so a radio past the edge left
the receiver bar off the top of the scale. The slice now stretches to wherever
the radio is.
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>
The README said "not yet" and listed four things. There are more than four,
and the more useful split is between what is half-built and what is finished
but has never met the hardware it talks to.
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>
Split. RadioState carries a transmit frequency, zero meaning the radio
transmits where it listens, so split is not a second flag that can disagree
with it. The contact stores it in N1MM's QSX column, the entry window shows
14008.00 followed by the transmit frequency, and the bandmap draws a red bar
there. SetSplitAsync sets the frequency before turning split on, or a radio
last split somewhere else transmits there.
Every command now goes out with a + in front, asking rigctld for its extended
answer: named fields ended by an RPRT line. The raw answer is bare values with
no terminator, so the client had to know how many lines each command returns —
there was a `command == "m"` special case for the one that returns two. Add a
third such command and get the count wrong once, and every later answer is read
against the wrong command for the rest of the session. RigctldReply parses the
extended form and is tested on its own.
A radio that cannot do split answers RPRT -11. That is an answer, not a broken
connection, so the frequency and mode it did report still count.
Two radios. Settings hold a list rather than one host and port, with the single
radio an older settings file holds carried into it. Both radios are read and
both show on the bandmap, the active one green and the other orange, but only
the active one drives the entry window: the second radio moving must not drag
the operator off the station being worked. Ctrl+Tab swaps, and a contact
records which radio made it.
This is not a full two-radio operating position — no second entry window, no
alternating CQ, no audio switching. It is two radios read and logged correctly.
Tested against a stand-in for rigctld over a real socket, and looked at under
Xvfb with two fake radios, one of them split.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three things stopped it.
A contest is keyed by ContestNR, not by ContestID. ContestID is only the
ContestInstance table's primary key; DXLOG.ContestNR refers to ContestNR, which
is what N1MM's SQLWhereString matches on. The two agree in a fresh log and
drift apart in one that has been used for a while, so we were reading one
contest's header with another contest's contacts. In a real log of 56284
contacts, joining on ContestID disagreed with the contact's own ContestName
52029 times; joining on ContestNR disagreed 109 times.
N1MM names a contest per mode: CQWWCW, CQWWSSB, CQWWRTTY, never plain CQWW.
Our contests now use those names, and N1mmContestNames turns one back into a
contest and a mode when a log is opened. The name carries the mode, so it beats
the ModeCategory column. Plain CQWW still opens, because older versions wrote
it that way.
The log columns were sized by counting characters, which was too narrow for the
headers: those are drawn in the theme's font, not the grid's monospace. The
grid sizes them now, with the character count as a floor so a column of short
values does not collapse.
Bandmap spots now age from when they arrived rather than from the time written
in them. A node with a wrong clock, or one replaying its backlog on connect,
emptied the bandmap as fast as it filled it.
Checked by opening a real 56284-contact N1MM log under Xvfb and reading the
contest and contacts back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bandmap was a list of frequencies and callsigns. It is now a frequency
scale with the stations written out beside it and a leader line from each
callsign back to where the station really is.
BandmapLayout does the placing and has no UI reference, so the rule is unit
tested: put the label centred on its frequency, and if it would cover the one
above, push it below instead. That is N1MM's CalculateOffset, and it is what
keeps three stations a hundred hertz apart readable.
The scale stops short of the top and bottom edges, or the first and last
frequency numbers come out cut in half.
VfoMarker draws the receiver as a bar as wide as the mode passes, rather than a
line. Only the receiver is fed: the radio does not report a transmit VFO or a
second radio yet, so VfoRole has the other two roles ready and nothing draws
them. Band-plan colouring of the scale is left out because the segments differ
by ITU region and there is no band-plan table to read them from.
Checked by running the program against a fake cluster node under Xvfb and
looking at the result: twelve spots, three of them stacked with fanned leaders.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
N1MM splits the telnet stream on CR, not LF, which says some nodes send CR with
no LF after it. Against those the assembler was handing the whole session back
as one unfinished line.
Also adds "user:" to the login prompts, which N1MM matches and we did not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The old client read the socket with a StreamReader, so the option negotiation a
node sends on connect landed in the text as control bytes and made a mess of
the first lines. TelnetStream now answers it: it agrees to ECHO and
SUPPRESS-GO-AHEAD, refuses everything else, swallows subnegotiations, and
tracks what it already answered so the two ends do not reply to each other for
ever. A command split across two reads is still understood, because a socket
hands over whatever has turned up rather than whole messages.
LineAssembler keeps the tail that has no line ending yet. Nodes write their
login prompt as "login: " with nothing after it, so the old ReadLineAsync sat
waiting for a line that never came and only got through because the greeting
happened to mention a matching word.
Login now follows N1MM: it looks for LOGON, ENTER CALL, LOG IN and the rest,
and sends the callsign anyway after ten seconds if no prompt turns up. A
password is sent when the node asks for one and the operator configured one.
The commands go out after that, not straight after the call. A connection that
has heard nothing for four minutes gets a blank line so the node does not drop
it as idle.
The old prompt check matched "call" anywhere in a line, which any spot comment
could trigger.
SendSpotAsync sends the node's dx command. Alt+P, or Edit / Spot It, spots the
call being typed at the current frequency, or the last contact logged when
nothing is typed, and puts it on our own bandmap without waiting for it to come
back round from the node.
The spot parser now takes a line with no colon after the spotter, and finds the
time when DXSpider has put the spotter's grid after it.
The cluster tests run over a real socket against a node fake that sends the
negotiation, the prompt and spot lines.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ctrl+Y from the entry window, or Enter on a row in the log, opens the whole
contact in one dialog. It reaches the fields the log has no column for: QSX
frequency, name, QTH, comment, grid, power, radio number, run position, and the
country and WPX prefixes. Previous and Next walk the log without closing and
offer to save first.
The dialog checks values through the same QsoEditor the log columns use, so a
zone the contest will not take is refused the same way in both places.
QsoEdit now names the field that was refused, so the dialog can put the cursor
back in the right box, and QsoEditor.ApplyAll changes several fields at once.
Points and the multiplier flags are shown but not editable. They are worked out
from the rules every time the log changes, so a typed value would be lost on
the next edit. N1MM lets you type them; we do not, rather than offering a box
that does nothing.
A contact's own country prefix now wins over the country file when scoring.
Without this, correcting the country in the dialog would change what the log
shows and what Cabrillo says, but not the score.
LoggingSession.Update now broadcasts the change like Edit does, so a contact
saved from the dialog reaches the other stations.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Double-click a cell in the log window to change it. The columns follow the
contest exchange instead of being fixed in the XAML, so CQ WW gets a Zone
column and Sweepstakes gets Nr, Prec, Ck and Sec.
QsoEditor holds the column list and applies one field edit. Validation comes
from the ExchangeFieldKind the contest declared: a CQ zone is 1 to 40, an ITU
zone 1 to 90, a section is looked up in the ARRL list, a grid must parse.
A refused edit returns the reason and leaves the log alone, so the cell
reverts. Changing the callsign runs the country lookup again.
Delete, or the right-click menu, removes the selected contact after a
confirmation. Either way the log is rescored, so a multiplier the removed
contact held passes to the next contact that claims it.
Both go out to the other stations in N1MM's own messages: contactreplace
carries oldcall and oldtimestamp, contactdelete names the contact. An incoming
edit or delete is matched by contact id first, falling back to call plus
timestamp because N1MM does not know our ids.
Country, continent and the two prefixes were filled in twice, once when
logging and once when editing. They now come from CountryFields.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The function keys send through cwdaemon or a WinKeyer, with N1MM's message
macros. Escape stops sending.
Settings are read with the reflection serializer rather than a generated one:
the generated one hands back null for every property the file leaves out
instead of the value the property is declared with, which crashed the program
the first time a new setting was added.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Checked by opening a log this program wrote in N1MM 1.0.11031. Three things it
rejected:
- a log naming a contest with no row in the Contest table throws "No current
row" in Contest.FromRow, so the definition row is written whenever a contest
is opened;
- an empty overlay category is answered with "Invalid Overlay Category:", so an
entry with no overlay now says "N/A", and the dialog offers N1MM's list;
- the sent exchange omits the report, which is what N1MM's own contest dialog
asks for.
With those three fixed N1MM opens the log and shows the contacts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The entry window types contacts and colours them as they are typed; the log,
check, bandmap, score and packet windows read the same session. Contacts are
shared with the other stations of a multi-operator entry in N1MM's contact
message, so an N1MM station on the same network sees them too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nonemm.Session holds what the operator is typing, what the log says about it and
what happens on Enter, with no UI toolkit behind it. Nonemm.Rig talks to
hamlib's rigctld and reconnects on its own. Nonemm.Spotting reads DX cluster
lines into a bandmap that drops spots after an hour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ARRL DX, IARU HF, Sweepstakes, RTTY Roundup, NAQP and general logging join
CQ WW and CQ WPX. The Cabrillo QSO line is now built column by column by the
contest, because Sweepstakes puts the callsign after the serial number where
everyone else puts it first.
User-defined contests read the subset of N1MM's .udc settings that covers the
exchange, the dupe rule, points and up to three multipliers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Frequencies, bands, modes, callsigns, grid squares and the country file live in
Nonemm.Core. Nonemm.Contests holds the scoring engine and CQ WW and CQ WPX.
Nonemm.Storage writes N1MM's DXLOG schema, and Nonemm.Formats writes Cabrillo
3.0 and reads and writes ADIF.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>