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>
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>