Commit Graph

7 Commits

Author SHA1 Message Date
ac63987e0c Move and batch spots the way N1MM does
Two differences from N1MM in the spot path, both found by reading Packet.cs.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 07:33:55 +00:00
da9884ebf5 Open a log N1MM wrote
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>
2026-08-27 16:27:35 +00:00
b9f0166777 Draw the bandmap the way N1MM draws it
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>
2026-08-27 16:15:01 +00:00
2b5ab54e94 Break cluster lines on a bare carriage return
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>
2026-08-27 15:53:23 +00:00
2b8033ccdd Speak telnet properly to the cluster node
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>
2026-08-27 15:46:19 +00:00
Erik
ffeeb2cdc1 Add the session, radio control, bandmap and cluster
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>
2026-08-27 10:39:53 +00:00