Ctrl+wheel and Ctrl+plus / Ctrl+minus change the text size of the
window in front, and Ctrl+0 puts it back. Each size is kept in the
settings file.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With a call in the entry window, a move of more than 2 kHz on the same
band puts the call on the bandmap where it was heard and wipes the boxes,
as N1MM does. The spot goes to the other stations as ADDSPOT and to the
Spots broadcast, never to the cluster. ADDSPOT from other stations is
added to the bandmap.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Listen for Radio broadcast data from another Nonemm or N1MM computer and
record its frequency, mode and split as this computer's own radio. No
radio is controlled. The settings are in the Configure Radios dialog.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Remove the status bar and the band name, add the 75 and 150 kHz spans,
add buttons for the callsign size (saved in the settings), draw the scale
in the form's text colour so it shows in the dark theme, mark the carrier
on the VFO bar and place a sideband bar on its side of the carrier, and
show who spotted a station, when, and the comment when the pointer is on it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The wheel steps along the span list and keeps the frequency under the
pointer in place. With Centre on VFO off, the slice stays put until the
receiver nears an edge. The span and the toggle are saved in the settings.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Going down N1MM's published list, the ones a contest operator would miss:
| | |
|---|---|
| `{LASTEXCH}` | what the last station sent, built from the boxes the contest asks for |
| `{ROVERQTH}` | where a rover is now |
| `{TIME}`, `{DATE}`, `{TIMEGMT}`, `{DATEGMT}` | the clock and the calendar |
| `{ESC}`, `{CTRL-A}` to `{CTRL-Z}` | the control characters a TNC reads as commands |
| `{CTRLHOME}` | the cursor back in the callsign box |
| `{F1}` to `{F12}` | another of this radio's keys, where `{CTRLF1}` is the other radio's |
| `{CQ}` | makes any key a CQ key: it starts running and carries alternating CQ on |
| `{DIGQTCS}`, `{DIGQTCR}` | opens the WAE QTC window the way asked for |
| `{CLEARRIT}` | the receive offset back to zero, which is rigctld's `J 0` |
| `{FORCELOG}` | logs what is in the boxes, holes and all |
`{FORCELOG}` is the one that gives something up. `LogContact` refuses an entry
that is short or holds a value the contest will not take, so that a caller who
has not checked cannot log nonsense; `LogContactAnyway` is the way past it,
because a contact in the log with a hole in it can be filled in afterwards and
one that was never logged cannot. It still needs a callsign, and the status
line says the row will need fixing before the log goes in.
A key that names itself would send for ever, so a key sending another key
ignores the macro a second time down.
What is still passed over is written down in the README and unfinished.md.
Advanced voicing is the one that matters: N1MM says a callsign or a serial by
playing a recording per letter out of `LettersFiles`, which is why `!` and `#`
say nothing on phone here and why F5 is empty in N1MM's own SSB file too. The
three packet BBS date formats are passed over because there is nothing here to
check a guess at them against.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
A phone function key names a wav file instead of words, which is what N1MM's
own SSB file holds: `F1 CQ,{WAVDIR}\Cq.wav`. `{WAVDIR}` is the folder named
after whoever is at the microphone, a name is taken relative to `Wav`, and
commas play several files in a row. An SSB `.mc` file written for N1MM plays
here.
Window ▸ Voice Keyer lists every recording the messages name, says which of
them are there for the operator picked at the top, plays one, and records over
one. The recording is written beside the wanted file and moved over it at the
end, so a recorder that will not start takes nothing away.
`CommandSoundCard` runs `aplay` and `arecord`, and both commands are settings:
the audio path belongs to whatever the station runs, and a library would pick
one of them for the operator. A recorder is stopped with an interrupt so it
closes its wav file, and killed only if it ignores that. PTT is cwdaemon's
`<ESC>a` for as long as the recording plays, off for a station on VOX.
Two things follow. The QTC window sends on SSB: N1MM's four recordings on the
Ready, Agn, Cfm and Close buttons, with the lines still read out by the
operator. And alternating CQ duels on phone, because the voice keyer says when
a recording has finished — which it had to, since the CQ message is picked by
the radio's mode and would otherwise have been keyed as Morse.
Nothing here has been played on a radio: this machine has no sound card, so
what is tested is the program being run, the file filled in, the exit code and
the interrupt, with shell commands standing in for the two.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
The QTC window put nothing on the air on RTTY except the lines themselves,
so the operator typed the rest into the digital window's transmit pane and
copied the lines by hand. N1MM sends a message for each step and fills the
lines in from a click. Both are here now, with N1MM's messages, N1MM's
defaults and N1MM's settings behind them.
The messages, all of them settings in the QTC window's setup area:
- RX Ready, `WAERXReadyText`, `{ENTERLF}I AM QRV BK`
- All Agn, `WAEAllAgnText`, `{ENTERLF}ALL AGN ALL AGN BK`
- Agn n, `WAEAGNText`, `{ENTERLF}AGN QTC: $ $ $ `, where `$` is the line
- Save, `WAESaveQTCText`, `{ENTERLF}R R R ALL OK BK`
- R U QRV, `WAERUQRVText`, `{ENTERLF}R U QRV BK`
- Save, reading traffic out, `WAESSaveQTCText`, `{ENTERLF}GL DE {MYCALL}`
Each keys the transmitter, sends the text and drops it again, and an empty
one sends nothing. The line spacing default is N1MM's `{ENTERLF}` rather
than `{ENTER}`, which was wrong.
The buttons: R U QRV and Snd Hdr were hidden while reading traffic out on
RTTY, though that is where N1MM starts — R U QRV is the first button
pressed. All Agn is new, and so is the S button on each line, which strikes
a line through a QTC that arrived too broken to read so the rows after it
keep their places; Agn n clears the strike and asks for that line again.
Clicking a line in the digital window fills the next row, which is how the
series is copied on RTTY: the other station sends the header and all ten
lines in one transmission. A click anywhere in a line hands the whole line
to the QTC window, which reads the time, the callsign and the serial number
out of it by shape and flashes the line to show it was taken. A line that is
not a QTC is left to mean what a click always means there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
The program stored an operator's window positions but never used them at
startup: the entry window was not placed and no window was reopened. It
now restores the whole screen both at startup and on a change of
operator: the windows that operator had open, where he had them, and the
ones he did not have closed.
The second radio's entry window no longer saves the layout. It has none
of the other windows open, so saving from it dropped every window the
first one had.
Ctrl+O gets six buttons for the operators last at the radio, newest
first. One click changes to that operator.
The recordings folder is no longer asked for. It is `Wav/<callsign>`
under the data folder, which is where N1MM plays them from, so
`StoredOperator.WavPath` is gone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
The log showed the contest's columns and nothing else, with the country,
points and multiplier columns fixed in the XAML. LogColumns now holds every
field the log can show and the settings file keeps the names, left to right.
A right click on a header adds or removes a column and offers the contest's
own set back, a header can be dragged to move its column, and a click sorts
on it. Sorting uses the field, not the text of the cell, so 9,000 kHz comes
before 14,000 and the serial numbers do not run 1, 10, 2. Both grids get the
same columns, so a value in the pane below sits under the column it belongs
to. Removing the last column is refused, otherwise nothing could be selected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
N1MM's statistics window. `StatisticsPivot` counts the contacts under one row
field and one column field, with the row total, the running total and the
column totals N1MM prints. The fields are N1MM's own two lists: 29 down the
rows, 20 of them across the columns, sorted the way N1MM's SQL sorts them, so
zone 10 comes after zone 2 and 80M before 20M.
The window has N1MM's two tabs. The table is the grid with `Tot` and `Accum`
and a `Total` line; the graph is a stacked column per row, one segment per
column, in one of N1MM's twelve palettes. "Include Dupes & X-QSOs" counts the
contacts left out of the claimed score and the ones marked as dupes.
Checked against a 3775-contact CQ WW SSB log: 3762 counted, which is what
`select count(*) ... where CLAIMEDQSO = 1 and ContactType <> 'D'` returns, and
the per-band totals match band by band.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
Seven tabs now, in N1MM's order, and the messages behind them.
**Options** holds what there is something behind: Log QSOs at all Stations,
which decides whether the contacts the others log are kept here; the
message trace, moved off the Messages tab where N1MM does not keep it; and
Run1/Run2, which used to be guessed from the radio number and is now the
operator's choice, sent in every `STATUS` and every `XMIT`. The transmit
interlocks stay out: this program does not block a transmitter, and a
station that claimed it did would be waited on for nothing.
**Call Stack Targets.** A callsign heard at one station and handed to the
position that will work it — `STACKANOTHERCALL` with the call alone,
`STACKCALL` with the contact behind it. The target is picked from the tab
or from the station list, and what arrives goes on the call stack of the
radio the operator is on, sorted by whether it is a multiplier here.
**The master** is the station the entry follows for cluster spots and the
time. `MASTER` names it to everybody, so the column agrees everywhere.
**The clock.** `TIME` carries a station's clock and Force Time Sync sends
it. N1MM sets the clock from it when it is running as an administrator and
says how far out it is when it is not; this program always does the second,
because setting the clock needs root and a contest logger has no business
asking for it. Anything over two seconds is called out.
**Serial numbers.** Reset Serial Numbers at all Stations sends
`RESETQSONRS` and every station starts again at one. The contacts already
logged keep the numbers they were sent with.
**What the entry agrees on** is now checked rather than assumed:
| What | How |
|---|---|
| the contest | `CONTESTNAME` as soon as a connection opens, and again in every `CHECKSUM` |
| the country file | its issue date — the publisher's `=VER20260826` — rides in `STATUS` |
| the computer name | longer than N1MM's fifteen characters is named at startup; a second computer answering to this one's name is named when its beacon arrives |
| the station number | two stations on the same number are named when the second says so |
`CountryFile` reads its issue date, which nothing had asked it for before.
Left for later: the master relaying its cluster spots, and the shared
serial-number pool where stations claim numbers in advance.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
Three pieces on top of the link:
**The network status window**, on the entry window's Window menu. A row
per station, this computer included the way N1MM shows it: number,
address, operator, band, mode, run, transmit, pass frequency, the last
message type, how long ago it arrived, the counts each way and the echo
round trip. A station that broadcast the wrong version is named under the
table in red — that is the one fault where everything looks connected
and nothing arrives. The box at the bottom sends a line of chat, and
Echo asks every station whether it is there.
**Where this station is** goes out once a second when it has changed.
The frequency, the mode and run have a dozen places they can change from
— the radio moving, a band button, a QSY typed into the callsign box —
so it is read and compared rather than announced from each of them.
**The ten-minute rule.** `BandChangeRules` already counted changes and
the stay on a band, and the entry window already showed the countdown,
but only a user-defined contest carried a rule, so every built-in
contest allowed anything. `ForCategory` gives a multi-operator entry
with one or two transmitters a ten-minute stay, which is N1MM's fallback
in `ContestInstance.BandChangeTimerDuration` for every contest that does
not name its own. Its per-contest table is a few hundred cases in a
decompiled hash switch and is not repeated.
Config ▸ Edit Networked-Computer Names now covers both networks: the
12060 broadcast to other programs and the 12070 link, with the station
number, the port, the version to claim and stations named by address for
a network where a broadcast does not reach.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
N1MM's Send All: the whole series as one message — the heading, every
line with the operator's spacing between them, then the ending, with
`{TX}` and `{RX}` around it so it keys the transmitter and drops it
again. The shape is N1MM's, from `QTCWindow.cs:3609`. A line is the three
fields joined with hyphens, which is how N1MM writes them, and Snd n
sends one line again for a station that missed it.
One message rather than a button per line because a series is close to a
minute of transmission at 45 baud: the type-ahead buffer holds it and
paces the engine.
Three settings with N1MM's names and defaults — `WAESendAllHeadingText`,
`WAESendAllEndingText` and `WAESQTCSpacing` — on the QTC window's setup
dialog. `{ENTERLF}` is new to the expander and stands for a carriage
return and a line feed; `{QTC}` is filled in by the window, which is the
only place that knows which series is going out.
The messages are tested as text. Nothing has gone on the air.
Left out: N1MM's four SSB recordings, which need a voice keyer, and its
RTTY messages for the station taking traffic down, which are typed into
the digital window's transmit pane here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
The engine probe answered two questions on the air, and they point opposite
ways.
TxBufLen is the number of characters left to transmit. Seventeen characters
pushed, five decoded back, and it read 12, counting down to 0 as the message
went out. So the pump no longer counts character times off the clock: it asks
the engine, feeds while the answer is under Lead, and asks again. The baud rate
is now only the fallback for an engine that will not answer.
A backspace is not an edit. Pushed in as a character it made the count go up by
four and the text went out unchanged, so text the engine has been given cannot
be taken back, only aborted. EngineTypeAhead was built on the opposite belief
and is gone, along with the setting that chose it and the interface that existed
to switch between the two.
A count that stops going down means the engine is holding what it has: MMTTY set
to Word out keeps a word until the space after it. The pump feeds one character
per look while that lasts, so the space arrives and the word goes out rather
than the message sitting there.
The probe kept two bugs of its own that this run showed: it read the count a
millisecond after pushing, saw 0 and called the message finished, and it started
a step while the previous unkey was still in flight, so that step ran with the
engine down. It now needs two empty answers in a row and waits for the engine to
say it has stopped. It also asks what {RX} does to a word the engine is holding,
which decides whether a macro without a trailing space loses its last word.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtspmWmS7f8kUvcyaHpRWZ
MMTTY has a type-ahead buffer of its own: characters go into it, a backspace
takes back one it has not transmitted, and TxBufLen says how many are left. Used
that way it paces itself, so there is no gap between characters to tune and no
baud rate to keep in step with the engine.
EngineTypeAhead does that, and Config > Digital picks between it and the pump
that is there now. Off is still the default: three things it rests on have never
been seen with a real engine.
tools/Nonemm.EngineProbe asks the engine those three questions and writes the
answers to a file. It starts MMTTY through the bridge, pushes a message, polls
TxBufLen while it goes out, backspaces over text that has and has not been
transmitted, and logs what came back on the receive side and when.
The bridge learns one verb for it: `buffer` reads TxBufLen and answers with the
count, or -1 when the control will not say. A property the control does not know
is a log line rather than an error, since it stops nothing.
TypeAhead and EngineTypeAhead share the TransmitBuffer interface, which is what
the digital window now works through, so the window does not know which one it
has.
docs/unfinished.md states what each buffer assumes and how to run the probe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtspmWmS7f8kUvcyaHpRWZ
The transmit pane is now the type-ahead buffer. What has gone to the engine is
in red beside the box and cannot be reached; what is still to go is in the box
and can be typed over, added to or deleted while the engine works through it.
A macro fills the pane rather than the engine, so the operator can correct a
call the message is still sending.
The cursor holds the pump: nothing behind it goes out, so an engine that
catches up with the operator idles instead of transmitting half a word. With
the box out of focus there is no cursor and everything goes.
Escape and the RX button drop what has not gone out and stop the engine where
it is. Enter puts in the carriage return the engine takes as a new line. The
pane starts empty for the next message.
The baud rate the pump counts by is in the digital setup window, beside the
alignment frequency. It has to match the engine: 45.45 unless the contest says
otherwise.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
The settings N1MM keeps for its digital interface: how many message buttons to
show, which of the two window scroll types to use, hover mode, the callsign
pause routines, what a callsign is checked against before it is highlighted,
which files that check reads, whether the grab list is cleared on a CQ or a
QSY, what happens to AFC and the mark tone on a run to S&P change, and whether
the receive pane is copied to a text file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
Something already sitting where Documents/N1MM Logger+ should go — a file, or
a link to somewhere that is not there — threw out of Directory.CreateDirectory
before the program had a window, so the operator got a stack trace and nothing
else.
CreateFolders answers with what stopped it, and the program shows it in a
window naming the path and stops there. NONEMM_HOME moves the whole lot
somewhere else, for a station that keeps its files off the Documents folder.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
The root is Documents/N1MM Logger+ now, so a station that runs both programs
has one set of files rather than two: one Databases folder, one SupportFiles,
one Wav with a folder per operator in it. What N1MM has no place for goes in a
Nonemm folder inside it, which is where settings.json sits.
A system that does not say where Documents is — most Linux systems — gets
~/Documents/N1MM Logger+.
Files an older version kept in Documents\Nonemm or ~/.config/nonemm are copied
in the first time this version runs, and the log and call history paths in the
settings are moved with them, so the copy that opens is the copy that came
over. The older folder is left as it was.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
The folders under the user's own directory are N1MM's now, with N1MM's names,
read out of N1MMLibrary.dll: Databases, SupportFiles, UserDefinedContests,
FunctionKeyMessages, CallHistoryFiles, ExportFiles, GoalFiles, LettersFiles,
QsoRecording, SkinsAndLayouts, SystemFiles, TransactionLogFiles, Diagnostics
and Wav. A station that runs both programs keeps its files in one shape.
An operator's recordings go in Wav/<his callsign>, which is where N1MM plays
them from, and the folder is made when he takes the radio. Cabrillo and ADIF
start in ExportFiles, call history files in CallHistoryFiles, and the .mc
function key files in FunctionKeyMessages.
N1MM's three *DDL folders and its Piper folders are left out: they hold the
migrations for N1MM's own admin databases and the program it speaks with.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
Ctrl+O asks who is at the radio, as it does in N1MM, and the callsign goes on
every contact logged from then on. A multi-operator station changes hands every
few hours, so each operator also keeps what he wants the program to look like:
where the windows sit, the theme, and the folder his recordings are in.
The windows on the screen are stored against the operator leaving, before the
dialog opens, and the operator taking over gets his own back. A window he opens
later is placed as it appears. An operator who is new to the list starts with
whatever is on the screen.
Nothing plays the recordings yet, so the folder is stored and waits on voice
keying. {OPERATOR} in a message sends the callsign.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
MMTTY and 2Tone have no socket or pipe interface: N1MM hosts XMMT.ocx and
exchanges window messages with the engine. A Linux process cannot load that
control, so bridge/nonemm-mmtty-bridge.exe hosts it under Wine and passes
lines over its standard input and output. docs/digital-bridge.md states the
protocol and what the control needs.
The window is N1MM's: receive pane with coloured callsigns, grab list, call
stacking, twenty-four macro buttons and the engine controls. One left click
copies what is under it — a callsign to the callsign box, anything else to the
exchange box the contest keeps for that kind of value.
Config > Digital registers XMMT.ocx in the Wine prefix on its own, making the
prefix first if it is not there. The engine, the bridge and the control start
at the copies shipped beside the program.
The bridge reads the control's own events. OnTranslateMessage carries only the
messages the control has no event for, so nothing was ever decoded through it.
hamlib's data mode names read as digital modes now, and a mode typed into the
callsign box changes mode the way a frequency changes band, so a station with
no radio can reach RTTY at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
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
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 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>
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>
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+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>
N1MM's format, so a file published for a contest is read as it is: # comments,
!!Order!! naming the columns of the lines after it, semicolons in preference to
commas so user text can hold a comma, and a default column order for a file
that names none. The directives that change what is stored are acted on;
!!Order!! can appear again part way through, because these files are usually
several files stuck together.
Leaving the callsign box fills what the file knows into the exchange boxes that
are still empty. A box with something in it is left alone: what the station
actually sends beats what somebody published months ago. The zone comes from
the CqZone or the ITUZone column depending on which the contest asks for, and a
file with no Sect falls back to State.
The check window's two missing columns are in. History lists matching calls
from the file. Exchange is not about callsigns at all: it offers the values the
exchange box with the cursor in it can hold, which is what N1MM's exchange pane
does — it searches the contest's list of valid exchanges, not the log.
CheckCandidate.Call is now Text, because an exchange value is not a call.
The section-validating directives are read and passed over rather than acted
on. Acting on them means holding N1MM's section lists and its rules about
retired sections, and getting that wrong throws away good data.
Running it caught what the unit tests could not: the exchange was filled and
scored but the boxes on screen still looked empty, because nothing resynced
them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LoggingSession mixed two things: the contest, which one station has one of,
and what the operator is typing, which belongs to a radio. It is now
ContestSession and RadioPosition. Two positions share one log, one score and
one run of serial numbers, so a station worked on radio 1 is a dupe on radio 2.
Frequency, mode, split, what is typed and whether you are running belong to
each radio on its own.
A second radio opens a second entry window. It has no menu of its own: it is
another view of the same contest, not another program. Only the window for
radio 1 opens the database and the connections.
Ctrl+Tab moves the operator to the other radio and the keyboard follows.
Ctrl+Shift+Tab puts both radios in the headphones.
OtrspBox speaks OTRSP to an SO2R box over a serial port: TX1/TX2 for the key,
RX1/RX2 and RX1S for the headphones, AUXnnn for an output line. It follows the
active radio, and every message points the box at this radio before keying, so
a message cannot go out of the radio the operator has just left. A command that
would change nothing is not sent, because the box works relays; N1MM does the
same. The box takes a Stream, so what it sends is tested without a serial port.
Still missing: alternating CQ, and voice keying on the second radio.
Looked at under Xvfb with two fake radios, one of them split: both windows up,
radio 1 showing 14008.00 into 14020.0, radio 2 on 14030.00, Ctrl+Tab moving the
keyboard between them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
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>