REF, the Ukrainian DX contest, the BARTG Sprint, ARRL 10M, Russian DX RTTY,
RDAC, YO DX HF, Oceania DX, IARU Region 1 Field Day, SARTG RTTY, All Asian and
SA 10. Every contest name in the log now opens, and every one of these but
Field Day scores its log contact for contact as N1MM did.
Most are written from N1MM's own class and checked against the log. Three
things the log settled that the class does not say plainly: ARRL 10M counts its
multipliers once per mode rather than once, the BARTG Sprint counts a continent
once in the contest while its countries and call areas count per band, and RDAC
counts nothing but the districts and scores nothing for a station that sends
none.
All Asian and SA 10 have no class in N1MM, which logs them from a .udc file, so
those two are written from the published rules and the log. docs/unfinished.md
lists what is still not settled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
Four more of the contests in the station's log. IOTA, CQ 160 and Mexico RTTY
score every contact in it exactly as N1MM did.
IOTA pays 15 points for a station that sends an island reference and 2 for one
that does not, and counts each reference once per band and mode. CQ 160 is one
band, so its states and countries count once each. Mexico RTTY and CQ 160 both
take a state from one side of the contest and a serial or a zone from the
other, in the columns N1MM keeps them in.
The EU DX Contest is written from N1MM's class, including its list of what
counts as Europe: the European Union, its outermost regions and its overseas
territories, and nothing else. The station's own logs were made with the .udc
file, which scores and counts two things differently; docs/unfinished.md says
which.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
Both are in the station's log and neither could be opened. The rules are
written from the published ones and checked by replaying the log.
YOTA scores a contact by the age the other operator sends: 13 points down to 10
for anyone 25 or under, whatever the continent, and 1 point or 3 for everyone
else. Every age worked on a band is a multiplier. The 2024, 2025 and 2026 logs
come out to the contact.
OK/OM DX is the first contest whose exchange differs by the other station's
country: a county from OK and OM stations, a serial number from everyone else,
in the two columns N1MM keeps them in, and `SkipsField` walks the entry window
past the box that does not apply. Points depend on which side the operator is
on, and Czechia and Slovakia count as the two countries they are. The 2023 log
comes out to the contact.
The Cabrillo exchange now gets the entry, because YOTA sends an age that only
the contest setup knows.
Two differences are left in the log, both from the .udc files the operator
used: their YOTA file only lists ages 7 to 25, so a younger operator scored as
an adult, and one OK/OM log was made with the file for stations outside OK and
OM while signing an OM call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
The three differences left after replaying the log were ours on purpose. A log
has to score the same in both programs, so they now follow N1MM:
- ARRL DX and CQ WW RTTY count the 14 Canadian areas N1MM lists as `48SDC14P`,
which keeps Newfoundland apart from Labrador. A station sending the postal
code is counted as the area the contest names.
- A maritime mobile is scored from the call it signs. CQ WW says it counts for
the zone alone; N1MM gives it the country, and so do we.
- A callsign the country file cannot place counts as a multiplier with an empty
value, so every unplaceable call worked on a band shares one multiplier.
29 of the 37 contest instances in the log now agree with N1MM on every contact
and on the totals. What is left is the country file of the day and rows whose
cached points N1MM never worked out again after a call was corrected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
Two pieces of work on the scoring engine.
The .udc reader now takes N1MM's published scoring vocabulary: the full
PointsPerContact condition set, the PointsMultBy family, PowerMult, BonusPoints,
the multiplier sources and the settings that say which stations bring a
multiplier in. MultMult is a weight rather than a switch, as in N1MM's
ComputeScore. The entry categories and the sent exchange reach the rules in a
new ContestEntry, so a contest can score by the power category it is entered in
or by what this station sends.
Then every contest in a real N1MM log was scored again from these rules and
compared with the points and multiplier flags N1MM wrote. That found five bugs:
ARRL DX and IARU read the exchange from the wrong column, a stored " " was read
as a value rather than as empty, CQ WW RTTY's own Canadian area names were not
counted, a maritime mobile scored nothing, and a contact in a mode the contest
does not run scored as if it were in the contest.
docs/n1mm-interop.md has the check and what still differs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
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
Three parts, as N1MM has them: what just happened on the left, the contacts
and multipliers in the middle, the score on the right.
The left says where the call being typed is from, in N1MM's own shape:
OM: EU -> Slovak Republic, Zn 15. The zone is the one the contest counts in, so
Contest.UsesItuZones answers that the way N1MM's ZoneType does, and IARU
overrides it. A message from something the operator just did stands there until
the next keystroke.
The middle is N1MM's breakdown: the contacts and then one number per multiplier
the contest counts, separated by slashes, and No Score before the first
contact. Points are not in it, because they are not in N1MM's; the score window
has them.
Those numbers were in the line at the top of the window, which now carries the
contest name alone, and the country was in the bar under the callsign box,
which now says only what working the call would bring. Neither is said twice.
Looked at under Xvfb: the country line for OM3KFF and then G4AB, 10/7/8 and
300 beside them, and a Mark message standing in the left until the next
keystroke.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
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
The twelve function keys were one row of twelve at the bottom of the window,
with the action row under them. N1MM puts both between the entry boxes and the
bearing line, six keys to a row in two rows, each row filling the width.
The action row takes N1MM's widths as well: 23 per cent to the first button and
the rest shared out. N1MM gives the remaining seven 11 per cent each; there are
five here, because Mark and Store are missing, so they share the same 77.
Looked at under Xvfb against the N1MM screenshot: same two rows of six, same
action row under them, same order down the window.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
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
Colours came from static brushes scattered over the windows. They now come
from a Theme, and everything that paints a station, a spot or a log row
asks Themes.Current as it paints, so changing the theme changes every
window without reopening it.
light is N1MM's own palette, read out of ApplicationStyles: the pale blue
behind a window, ivory on a button, white boxes, and the four colours a
station is painted in. dark is the same states in colours for a dark
screen, which is roughly what the program looked like before.
Config > Manage Skins, Colors and Fonts picks one, under N1MM's name for
the same dialog, and shows what each theme paints a callsign. The choice
is kept in settings.json as Theme, and light is the default.
The window chrome follows through resources the Fluent controls read, so a
text box and a button take the theme's colours in every state. Fonts and
skins are not part of it.
Looked at under Xvfb in both themes: the entry window, the theme dialog,
and the log window with its rows still coloured by what each contact was
worth.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The colours are N1MM's own defaults, read out of ApplicationStyles: blue
for a station worth working, red for one new multiplier, green for more
than one, grey for a dupe, and red on the CQ frequency line. A call under
three characters stays blue, as N1MM leaves it.
The mark at the right-hand end of the callsign box is N1MM's OKCall: a
tick when the call is in MASTER.SCP, a question mark while it is not, in
the colour of the call. Like N1MM it only shows while the check window is
open, for three characters or more, and only while the call is short
enough to leave room for it.
The band under the boxes takes N1MM's background colours instead of white
on the text colours. The two lights, the telnet window and the message
editor were borrowing the verdict brushes for things that are not
verdicts; they have their own colours now.
The callsign and the exchange boxes uppercase what is typed. Name and
comment do not, which is what N1MM does.
Scored contest 63 of om5m.s3db (CQ WW SSB, 3775 QSOs) against the points
and multiplier flags N1MM stored: points identical, multipliers 601
against 602. The difference is B1Z on 10m, which N1MM logged with an
empty country prefix and counted as a country of its own though BY was
already worked on that band.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 dialog now carries N1MM's fields in N1MM's order, with the labels to
the left of the boxes and the tip text at the top right. New fields in
StoredStation: the address, the licence class, the position, the radio,
the antenna and the email address.
The grid square and the position fill each other in, as they do in N1MM.
Which box has focus decides the direction, because TextChanged arrives
too late to tell a typed edit from a programmed one.
The fields N1MM's form does not have are gone from it. Continent and the
country prefix are looked up from the callsign after Ok; the rest keep
the value they had.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three things you could not see or could not set.
Run and S&P were a label saying which one you were in. They are two buttons
now, the one you are in marked, either one clickable, which is what N1MM has.
Ctrl+? and F1 still work.
The tuning tolerance is one number per mode, as N1MM keeps it: CW, phone and
digital, 300 Hz each, which are its defaults. A phone signal is wide and a CW
one is not, so one number for both was wrong.
The bearing line went blank whenever it could not be worked out, which looks
exactly like a missing feature. It now says which end could not be placed and
what to do — no country file, no position for your own station, or a callsign
the country file does not place. It also gives miles beside the kilometres, as
N1MM does.
While clicking it: a marked button lost its mark under the pointer, because the
theme paints its own background there. The band buttons had the same fault.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tab was doing what space does, which is wrong. N1MM's EntryWindow does not
touch the tab key — ProcessCmdKey only remaps NumPad Enter — so tab there is
plain navigation through the boxes in the order they are laid out: the reports
at tab index 3 and 4, the name at 26, the section at 30, the zone at 32, the
comment at 40. Only space goes through NextTab, which is the walk that steps
over the reports.
So tab now walks every box, reports, name and comment included, and shift and
tab walk back. Space is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
The title bar said Nonemm. It now says what N1MM's says: the frequency, the
mode, whether the frequency came from a radio or was typed, and which radio the
window belongs to when there are two. Reading it off the taskbar is the point.
Under the function keys is N1MM's second row: Esc: Stop, Wipe, Log It, Edit,
Spot It and QRZ. Each one does what its key already did, so the keys and the
buttons cannot disagree. QRZ opens the callsign's page in a browser, and says so
in the status line when there is no browser to open.
Mark and Store are left out. They stack callsigns, and call stacking is not
here, so a button that did nothing would be worse than no button.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>