Files
Nonemm/docs/unfinished.md
T
erikbrockoandClaude Sonnet 5.5 0a23815f38 Lay out Available Mults and Qs like N1MM's window
The window is now a summary table of mults, Qs and totals per band over a
sortable spot grid with Call, Freq, Mode, Dir, Mlt, SN, TS, Spotter, Grid,
Sun, Pts and a preferred-spotter column. Columns can be hidden, headers
sorted, and the list stops re-sorting for six seconds under the mouse.

Adds the Mults / Mults & Qs button, a Bands & Modes filter, delete and
blacklist from the right-click menu, Alt+A stepping, and QSY to the top spot
after logging. Spot gains Snr and IsSkimmer, Bandmap gains Remaining, and
AvailableStation carries the path to the station.

The menu item no longer has apostrophes. SO2R, SO2V and the other missing
pieces are listed in docs/unfinished.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 11:56:32 +00:00

844 lines
51 KiB
Markdown

# What is not finished, and what has not been tested
Two separate lists. A feature can be finished and untested, or half-built and
exercised every day. Written 2026-08-27, updated 2026-09-11.
## Never run against the real thing
Everything below is written from a published protocol or from N1MM's source and
tested against a fake that speaks the same protocol. A fake agrees with whatever
the person who wrote it believed, so these are the places where a wrong belief
is still sitting there undiscovered.
| What | Tested against | What has never happened |
|---|---|---|
| `RigctldRadio` | a stand-in for `rigctld` over a real socket | no real `rigctld`, no real radio. The extended answer format, the `+` prefix and the split commands are from the documented protocol. If the format is wrong, nothing works rather than something being subtly off — check this first. |
| `OtrspBox` | a `MemoryStream`, checking the bytes | no real SO2R box. Command forms are from N1MM's `N1MMPort.cs`. |
| `ClusterClient` | a node fake over a real socket, sending the telnet negotiation, the login prompt and spot lines | no live cluster node. Which nodes send bare CR, and which send option negotiation, is guessed from N1MM's code. |
| `StationNetwork` | the message format, round-tripped | no second station, and no N1MM on the same network. |
| `StationLink` | the bytes of every message, and two links talking over loopback | no real N1MM has ever been on the other end. The version check is the thing to try first: N1MM turns away a station whose version is not its own, and this program has to be told what to claim. |
| `CwDaemonSender` | the UDP messages, and a fake daemon that answers the `<ESC>h` reply request | no `cwdaemon`, no radio keyed. |
| `MmttyEngine` and the Wine bridge | MMTTY 1.70 under Wine 10 on a machine with a sound card: started, keyed, a message transmitted and decoded back off the air, and stopped | no radio. No FSK through EXTFSK, no PTT on a serial port, and 2Tone has never been run. Keying was wrong until 2026-09-01: `SetMmttyPTT` stops a transmission and does not start one, which is why nothing ever went out before then. `docs/digital-bridge.md` |
| `WinkeyerSender` | the status-byte reader, on its own | no test of the serial side, and no WinKeyer. The host-mode open sequence is from the WinKeyer datasheet; the status bits are from N1MM's `Winkey.cs`. |
| `CommandSoundCard` and the voice keyer | shell commands standing in for `aplay` and `arecord`: the program being run, the file filled in, the exit code, the interrupt that ends a recording | no sound card on this machine, so nothing has been played or recorded, and nothing has gone on the air. cwdaemon's `<ESC>a` PTT is from its published protocol and has never keyed a radio. |
The Cabrillo output has not been put in front of a contest sponsor's robot.
## Half-built
**The digital window: what N1MM has and this does not.** The window is here —
receive pane in both of N1MM's scroll modes, the Letters/Figs and MouseOver
readouts, the pause bar, coloured callsigns with N1MM's validity routines, grab
list, call stacking, hover mode, the message buttons, the engine controls, the
text file in and out — and the function keys, ESM and the QTC window send
through the engine on a digital mode. Left out: the waterfall, the spectrum and
the XY scope, which the engine reports and nothing draws; more than one receive
pane, which is MMVARI's multi-channel decoding rather than MMTTY's; N1MM's
AutoTRXUpdate, which offsets the frequency the entry window and the log record
rather than only the one in the title bar. The other interfaces N1MM offers —
fldigi, MMVARI, the hardware TNCs — are not here either, though `DigitalEngine`
is the place to add them.
The receive pane has been driven with a stand-in bridge that prints prepared
text, which is how the printing, the colouring, the Letters/Figs box and the
type-ahead transmit pane were checked. Nothing has been decoded from a real
signal and nothing has gone on the air: this machine has no sound card, and
MMTTY does not start on it.
**The entry window's function keys on a digital mode.** They are the digital
macros, not the CW file: N1MM loads its send buttons from the RTTYBTN set on a
digital mode, and the CW messages carry no `{TX}`, so a key pressed there fed
the engine without keying the transmitter and nothing went out. The first ten
digital macros are F1 to F10; F11 and F12 stay Spot and Wipe, as in every other
mode, and the macros past the tenth are on the digital window's own buttons.
Right-clicking a key still opens the CW or phone messages, so the digital macros
are edited from the digital window.
**How the transmit pane works.** The pane is one editable box holding the whole
message: what the engine has transmitted, coloured red, then what is still to
go. The red stops where the air is, not where the feeder is: the last characters
handed over are still in the engine, and colouring those too turned every
character red as it was typed once the transmission had caught up. An edit that
reaches into the red is undone, and so is one into the characters the engine
holds but has not sent yet: the engine cannot give a character back.
The two are one text, so they wrap together and neither takes width from the
other. A character moving from one half to the other only moves the colour
boundary, so the caret and what is being typed stay where they are.
Nothing goes on the air until the transmitter is keyed, which is the TX button,
Ctrl+Enter or Alt+T. From then on the pane goes out and so does whatever is
typed into it, and a function key pressed meanwhile goes on the end of what is
already waiting. Enter is a new line in the message. The message ends when the
transmitter has been told to drop and the engine says it has: what went out is
cleared off the pane, what was typed ahead is kept, and nothing goes out again
until TX is pressed. A new message clears the pane the same way, for the case
where two of them run close enough together that the transmitter never drops.
`{RX}` runs when the message it stands in has been handed over, wherever it
stands in the macro. Every other action macro runs before the text unless it
stands after `{END}`, but the transmitter cannot drop before the text has gone
out. Running it in place, in front of the text, was what made the first click on
a CQ button beep for half a second, unkey, then send the rest: the stop was
issued while the engine held the two characters of the feeder's lead, so MMTTY
sent them and dropped, and nothing was left to stop the transmitter at the end.
The bridge log showed `> ptt 1` one millisecond after `> key 1` and before the
first character. N1MM does the same as this now: it takes `{RX}` out of the text
wherever it is, sends the text, then calls `StopTX`.
The transmitter drops on `{RX}`, on the RX button or on Alt+T, and all three do
what N1MM does at the end of a message, which is not what it does anywhere else.
N1MM hands MMTTY the whole message in one `SendString` and calls
`SetMmttyPTT(1)` with the message still in the engine's buffer. MMTTY then ends
the transmission itself, at the last character. It never asks MMTTY how much is
left; `TxBufLen` does not appear in N1MM at all.
What sits between the text and the stop there is nothing at two of N1MM's three
MMTTY send paths (`DigitalInterface.cs:19177` and `:19451`) and `sSleep(400)` at
the third, the macro buttons (`:21553`). The sleep is not the mechanism — a full
buffer is — and as a wait it is both too long on a long message and wrong on a
short one: `TU` is 330 ms of air at 45.45 baud, so 400 ms of sleeping puts the
stop after the buffer has emptied, which is where it does nothing. This program
asks instead. `DigitalEngineSender.WaitUntilHoldingAsync` sends the stop as soon
as the count says the engine holds something, and gives up after N1MM's 400 ms,
which is `StopDelay`.
So `{RX}` stops feeding: `TypeAhead.FlushAsync` hands over everything that is
left in one piece, and the stop goes out 400 ms behind it. Nothing after that
can be rewritten, which is what `{RX}` means. The feeder still paces the middle
of a message, which is what makes type-ahead work; only the ending is N1MM's.
`{RX}` runs as soon as the message has been given to the keyer, not after the
message has gone out. That is the whole point: waiting for the feeder to hand
the last character over leaves the engine empty, and a stop that reaches an
empty engine does nothing. `EntryWindow.SendThenAsync` takes `{RX}` out of what
stands after `{END}` and runs it there; the rest of `{END}` still waits for the
message to go out.
What the ending costs on the air, from a bridge log of one CQ: the last
character was fed at 9616 ms and went out at about 10606 ms, and the transmitter
did not drop until 11549 ms. That is 950 ms of dead carrier per transmission
paid on the old timer. The key-down fallback brings it to about 230 ms. N1MM's
ending costs nothing, because MMTTY drops the transmitter on the last character
itself.
The reason is that `SetMmttyPTT(1)` does nothing at an engine that has been fed
one character at a time and is therefore nearly empty. Probe question 9 sent it
with `TxBufLen` at 0 and MMTTY transmitted for another eight seconds; a bridge
log of three CQs shows the same on the air, with the transmitter staying up
until this program forced it down. Whether it is the empty buffer or the way the
text arrives that MMTTY objects to is not settled: probe question 11 sent N1MM's
exact sequence and it did not drop either, but that machine has no sound card,
so MMTTY never really transmits and its buffer reads 0 whatever it is given.
If the engine still does not drop the transmitter, the key goes down here: the
control's `PTT` property back to false, which took 251 ms in the probe. What it
waits for is the engine, not a clock: `StopPatience` is how long the engine may
make no progress, not how long the whole wait may take. Measured against the
whole wait it cut a CQ off with 21 symbols still in the engine, because a
flushed message is seconds of transmission and the engine is entitled to all of
it.
Two things had to be right before the flush worked at all, and both were wrong
first. The flush takes the same turn the feeder takes, so a character the feeder
had already taken and was still handing over cannot end up behind the rest of
the message — it did once, and the CQ went out with its first letter at the end.
And what the engine holds is counted across feeders rather than reset when one
starts: a feeder that started after a flush forgot a whole message the engine
was still holding, called the transmission over and put the key down in the
middle of it.
`DigitalEngineSender.WaitUntilAiredAsync` waits for `Aired` first, which is two
things at once: the engine's own count at 0, and the clock saying the characters
the engine held have had time to go out. `StopPatience` caps the wait for an engine
that never reports itself empty.
One character time is held after that and no more, because the count reaching 0
is late news rather than early. In a bridge log the count read 0 at 10670 ms and
the last character came back decoded at 11026 ms, and the decoder runs about
420 ms behind the air, so that character went out at about 10606 ms — before the
count read 0. What the character time covers is the reading, which happens every
`TypeAhead.PollInterval`, not the transmission. Turnaround time is worth more
than padding in a contest.
MMTTY has no stop character to send instead. Its macro language ends a
transmission with `\` at the end of a macro and stops the carrier with `~`, but
those are read by the macro interpreter, and the only way into the engine from
here is `PostMmttyMessage(4, ...)`, one typed character. Probe question 10 typed
both at the end of a message: both were swallowed and the transmitter stayed up.
Before this the stop was `AbortXmit` a fixed `StopPatience` after the polite
stop, which is a race. It was lost once in the log: on one of three CQs the
abort fired 4 ms before the count reached 0 and the carriage return at the end
of the message never went out.
Escape is the one that stops now: what has not gone to the engine is dropped and
the engine drops what it holds.
**What paces the feeder.** The clock, at one Baudot symbol every symbol time for
the baud rate in the digital settings, with the engine kept `Ahead` characters
ahead: one on the air and one in hand for when that one finishes. RTTY runs at a
fixed speed, so the clock is right.
A character is not a symbol. Most are one, but a digit sent while the engine is
in the letters shift costs a shift symbol and the digit, and the letter after it
costs a shift back: `OM5M` is six symbols, not four. Pacing one character per
symbol time ran ahead of the air by about one part in nine — 26 characters of
one CQ went out in 29 symbols — which showed up as the red text in the transmit
pane running ahead of the transmission, and as the engine's count building up
until the brake caught it. `TypeAhead.Symbols` prices a character against the
shift the engine is in, and both the feeder and the model of what is on the air
use it. They keep separate shift states, because the feeder is `Ahead`
characters in front of what is being transmitted.
**What a character costs cannot be calculated, so it is bounded.** The shift a
character needs depends on settings this program cannot see: unshift-on-space is
`TXUOS` in MMTTY's `UserPara.ini`, written per profile, and it is also a button
on MMTTY's own display that the operator can press during a contest. The FIG
button is another. `Symbols` assumes unshift-on-space is on, because MMTTY's
help says that is the usual setting and because assuming it charges a symbol too
many, which leaves the pane behind the air rather than in front of it.
The engine's count is what settles it, and it needs no setting to be read. The
count is in symbols and falls as they are transmitted, so what it drops between
two answers is what went on the air between them. That is the air's own rate,
measured rather than reckoned: `WentOutByCount` spends those symbols on the
characters at the front of what has not gone out and marks them as gone.
Two things make it self-correcting. The rate comes from the engine, so a clock
that runs a little fast or slow cannot drift. And a count of 0 means the engine
holds nothing, so everything it was given has gone out, whatever the symbols
added up to along the way — a wrong guess about the shift is squared off at the
end of every message rather than accumulating over a contest.
The one thing to be careful of is that the count trails what the engine was last
given by 100 to 150 ms, which the probe measured. A 0 newer than `CountLag` is
the answer not having caught up, not an empty engine, and is passed over. An
engine that does not count at all falls back to the clock.
Not the decoder. MMTTY hears its own transmission with sound loopback on and
reports it back, which would be the air itself, but that is the receive path: it
is off at many stations, and with a receiver actually listening another
station's `CQ` matches ours by chance. The transmit buffer count is the
transmitter's own report and has neither problem.
`MmttyEngine` has to declare `EngineBuffer` for any of the count to be used.
Without it the sender falls back to the plain path: whole `SendString` calls
instead of `PostMmttyMessage(4, ...)` one character at a time, and no count at
all. It ran that way for a while, and a bridge log of a CQ shows what it costs.
The feeder put 26 characters out at 165 ms each; MMTTY put them on the air at
183 ms each, because the digits in the callsign cost a shift to figures and the
letter after each one a shift back. By the end of the message the engine was
1.5 s behind, and `{RX}` was answered by the hard stop rather than by MMTTY
running dry. The brake is what holds that gap down.
The clock advances by exactly one symbol time per symbol handed over,
never from the time the poll happened. A poll is up to 50 ms late, and starting
the next character from there made every character a little late, the lateness
added up, and the engine ran dry and transmitted the idle tone between the
characters of a long word.
The engine's own count cannot be the pace, which the probe log shows twice over.
`TxBufLen` read 0 for the first 150 ms after twenty-one characters were pushed
and were already going out, and it read 0 for three seconds while the engine sat
holding `ABCD` on Word out. The count is only to be believed while it is large
and going down, and the feeder keeps the engine nearly empty on purpose — that is
what leaves the message editable — so it lives in the range where the count
reads 0. A feeder that took that 0 as room fed on it every poll and handed the
engine about three characters for every one on the air, which is what put the
red text ahead of the transmission.
So the count is a brake. `Slack` is how many symbols the engine may be behind
before the feeder waits for it: MMTTY answers in symbols, which is more than the
characters it was given, so an exchange full of digits is slower than the clock
thinks and this is what stops the clock running away on it. A count that is not
going down means the engine is holding what it has, and the brake lets go after
`HoldingPatience` so the space that releases a held word can get there.
What the engine probe established, with MMTTY 1.70 under Wine on a machine with
a sound card:
The answer being in symbols is why `Slack` is in symbols and the feeder keeps no
character count of its own to compare against it.
The report is worth reading for the receive side as well. With MMTTY's sound
loopback off, its help says the receive window is fed from the transmit window,
which would be a per-character report of what has gone out with no polling at
all. With loopback on, which is how this station runs it, the decoded text
arrives about 420 ms after the character goes out and matches what was sent.
**The protocol log.** Every line in and out of the bridge is written to
`Diagnostics/digital-<when>.log` under the settings folder, one file per run of
the engine, with a millisecond stamp on each. The buffer question is left out
and its answer written only when the count changes, so what is left is the
keying, the characters and the transmit reports. It is what says who dropped the
transmitter and when, which nothing else in the program can answer: the engine
probe cannot, because it runs the engine rather than the window.
```sh
./build.sh run --project tools/Nonemm.EngineProbe -- \
--engine ~/mmtty/MMTTY.EXE --prefix ~/.wine-nonemm --out engine-probe.log
```
It transmits on the sound card for about twenty seconds and keys no serial port
unless `--ptt` names one. `EngineProbe` states what each answer means.
**The transmitter dropping, and what ends a message.** `DigitalEngineSender`
holds three states — `Down`, `Keyed`, `Ending` — and its class comment carries
the transition table. The state is there rather than a flag because ending a
message takes as long as the engine takes to transmit what it holds, which is
seconds, and the operator can key again inside that time. With a flag, the
ending of one message went on running and put the key down in the middle of the
message after it. Each ending now belongs to one transmission and checks after
every step that it is still that transmission.
Coming back out of `Ending` costs something too. By then the stop is inside the
engine, waiting for its buffer to empty, and MMTTY fed while that stands stayed
keyed and transmitted nothing — the characters went in and never came out, and
the pane coloured them because `TxBufLen` answered 0, which is what an engine
that has transmitted everything also answers. So a macro pressed while the last
one is ending waits for what the engine holds to go out, clears the stop with
`AbortXmit`, and keys again. The wait is nothing at all in the case that shows
it: the last message has finished playing, which is why the operator pressed the
next key.
A message ends when `{RX}` or an abort has told the engine to stop and the
engine then reports the transmitter down. A report nobody asked for is
ignored. MMTTY reports the
transmitter down at other times — the buffer hands it one character at a time
and keeps it nearly empty — and taking that as the end cleared the pane and shut
the gate two or three characters into a message, leaving the rest of it to go
out on the next function key.
That report is not the only thing that starts the pane again, because it does
not always come. Two macros pressed one after the other keep the transmitter up
from the first to the last character, so the engine never reports a drop between
them, and the pane kept both messages. Everything the engine has been given is
locked — it cannot be taken back — so a run of macros left a pane with nothing
in it that could be edited. `DigitalEngineSender.StartAsync` now calls
`TypeAhead.Started` whenever it keys a new transmission, which drops the last
message off the pane and keeps whatever was typed ahead.
`TypeAhead.Ended` is called while the state lock is held, for the same reason.
The drop arrives from the engine on a thread of its own, and a macro pressed on
the last character of a message got as far as handing its own text over in the
gap between reading the state and clearing the pane: the new text was wiped off
the pane while the engine transmitted it. The order was found by reading the
code, not on the air, and the fix has not been tested against a real engine.
Which of the control's two paths carries the transmit state has not been
settled. The bridge takes it from `OnPttEvent`, and N1MM's `DigitalInterface`
binds an empty handler there and reads MMTTY's window message 32772 out of
`OnTranslateMessage` instead. Both are in the same class, so one of them is a
leftover, and `docs/digital-bridge.md` argues 32772 cannot reach
`OnTranslateMessage` at all while the control has an event for it. `OnPttEvent`
does report drops here, so nothing is missing; it is worth measuring only if the
reports turn out to be coarser than the message.
Keying is the keyer's, not the window's: `{TX}` and the TX button both go
through `DigitalEngineSender.Transmit`, so the order to key is always the first
thing the engine is told. MMTTY keys itself off a character given to it while
the transmitter is down and drops again when it has sent it, so a key that
arrived behind the text put a keyed-up gap in the middle of the message. A drop
in the middle of a message that is still going out also keys the transmitter
again, so what is left of the message does not go out into a transmitter that
is down.
The idle tone between what the operator types is MMTTY's own diddle, the Diddle
setting on its TX tab: LTR, the standard one, is what `mmtty/UserPara.ini`
carries here. Nothing is fed to produce it, so it needs the transmitter to stay
keyed. Whether MMTTY holds it there with an empty buffer has not been measured:
the probe run that says it does was on a machine with no sound card, where
MMTTY does not really transmit.
**The link between the logging computers.** N1MM has two networks and this
program now has both. Port 12060 carries XML to other programs — a spot tool, a
score poster — which is `StationNetwork`. Computer to computer, N1MM uses port
12070 and a different protocol: `StationLink`.
That protocol is a UDP broadcast to find the other computers and a TCP
connection to each of them, opened both ways. A message reads
```
DATA__07%SHACK-PC%QSO%2026-09-03 12:34:56%DL1ABC%…~__DATA
```
which is the sending station's number, its computer name, the message type and
the fields of that type. `%` and `~` cannot appear in a field, so N1MM writes
`!` in their place. What is read and sent: contacts, edits, deletes, resyncs,
`IAM`, the echo pair, chat, the pass frequency, transmit on and off, where a
station is tuned, the time of a station's last contact, the two serial number
messages, and the four messages a resync is made of. The rest of N1MM's
forty-odd types — the score and sked windows, the log check, the spot lists —
are passed over, which N1MM's own reader also does for a type it does not know.
**The version has to match.** N1MM compares the version in the beacon with its
own and, when they differ, puts up "Software versions must match. Update N1MM+."
and drops the station. So the version this program broadcasts is a setting —
Config ▸ Edit Networked-Computer Names — and it has to be the version of the
N1MM copies beside it. Help ▸ About in N1MM says which. A station that
broadcasts something else is kept in the list and the network status window
names it, because that is the one fault where everything looks connected and
nothing arrives.
Where a station is goes out once a second when it has changed, rather than from
each of the dozen places it can change from. In a multi-single entry that is
what the other operators watch. The frequency goes on its own in N1MM's `FREQ`,
in hundredths of a kilohertz; the mode, run and the rest ride in the `STATUS`
that follows it.
**The status message.** `STATUS` goes out every five seconds whether anything
has changed or not, and it is what fills the other operators' windows: the pass
frequency, the frequency, run or search and pounce, the operator, the rates over
the last ten and the last hundred contacts, the mode, the categories and the
version. `Rates.PerHour` reads the two rates out of the log the way N1MM does —
counting back over the last N contacts, stopping at a gap longer than the
contest's minimum off time, and treating a span under a minute as a minute so a
burst of three does not read as thousands an hour. The five transmit-interlock
flags in the message are sent as false: this program does not block a
transmitter.
**The network status window** has all seven of N1MM's tabs — Stations,
Resynchronize, Rescore, Options, Call Stack Targets, Actions and Messages — and
its columns — Computer, IP Address, Pass, Run, 10, 100, Freq, Op, Msg, Send,
Receive, Band, R/M, Mode. Right-click a row to pass the contact being worked to
that station, to talk, or to close and open its connection; a connection closed
by hand stays closed until it is opened again, because the next beacon would
otherwise open it within ten seconds. The Resynchronize and Rescore tabs work
on a period — the contest, since a time, or the last few hours. Options holds
what there is something behind: Log QSOs at all Stations, the message trace, and
Run1/Run2. Left out are the two telnet options, which wait on the master relaying cluster
spots.
**Two signals at once.** N1MM blocks the transmitter when the entry is about to
break its own class. This program does not touch the transmitter — a logger that
decides for itself when the operator may not send loses a contact every time it
is wrong — so the entry window puts a red box across its top instead, naming the
other station and the band. `TransmitRules` holds what is allowed: one signal in
a one-transmitter entry, two on different bands in a two-transmitter entry, and
never two on one band whatever the class. A station that has not said which band
it is on counts as somewhere else, because a guess there puts the box in front of
an operator doing nothing wrong.
**What the entry agrees on.** Four things have to match across the stations of
an entry, and each is now checked rather than assumed:
| What | How it is checked |
|---|---|
| the program version | the beacon, which N1MM refuses to talk past |
| the contest | `CONTESTNAME` as soon as a connection opens, and again in every `CHECKSUM` |
| the callsign | `CHECKSUM` |
| the country file | its issue date rides in `STATUS`; two issues score the same contact differently |
A computer name longer than the fifteen characters N1MM allows is named at
startup, a second computer answering to this one's name is named when its
beacon arrives, and two stations that have taken the same station number are
named when the second one says so.
**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.
**The master** is the station the entry follows for cluster spots and the time,
set from the station list and told to everybody, so the `Master` column agrees
everywhere. What it does not do yet is relay: N1MM's master passes its cluster
spots to the others over the link, and `PACKET` is not written. `ADDSPOT` is
written and read, but only for a local spot made on a QSY (see below); the
master does not relay its cluster spots with it.
**Local spot on a QSY.** With a callsign in the entry window, the radio moving
more than `QsySpotHertz` (default 2 kHz, N1MM's value) on the same band puts the
call on the bandmap where it was heard and wipes the boxes. It sends `ADDSPOT`
to the other stations and a Spots broadcast, never a cluster spot. Nothing
happens while running, in quick edit, with a complete exchange, with a number in
the box, or when the bandmap already has the call there. N1MM's other
trigger, a dupe left behind by a move of 0.1 kHz or more, is not done. There is
no dialog for the distance; it is in the settings file.
**Call stacking.** 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 Call Stack Targets 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.
**Serial numbers.** Reset Serial Numbers at all Stations sends `RESETQSONRS`
and every station counts from its own log again: the next number is the highest
one in it plus one, which is what the N1MM manual says the button does. The
contacts already logged keep the numbers they were sent with.
The shared pool is written: `ReserveNr` claims a number at the first character
of the callsign and `QSONRS` carries the whole pool when a station has fallen
behind. `RejectNr` is not — N1MM sends it and reads nothing, so there is
nothing on the other end to agree with.
**The ten-minute rule.** `BandChangeRules` counts band changes and the stay on a
band, and the entry window shows the countdown. What was missing was the rule
itself: only a user-defined contest carried one, so every built-in contest
allowed anything. `BandChangeRules.ForCategory` now gives a multi-operator entry
with one or two transmitters a ten-minute stay, which is N1MM's fallback for
every contest that does not name a number of its own, and a contest that caps
the changes per hour overrides `Contest.BandChangesFor`. N1MM's per-contest table
is not repeated: it is a few hundred cases in a decompiled hash switch, and the
contests this station enters are the ones worth reading out of it one at a time.
**Resync.** A station that was away asks the others for the hours it missed.
The walk is N1MM's, an hour at a time:
| Message | What it says |
|---|---|
| `CHECKSUM` | here is what I hold for your contacts in this hour, and the contest and callsign I am logging |
| `DELETEQS` | drop your copies of my contacts in that hour, I am sending them again |
| `RESYNCQSO` | one of those contacts |
| `CONFIRMED` | here is what the hour hashes to at my end — which also asks for the next hour |
Each station is the authority on the contacts it made, so the answer replaces
the block rather than merging into it. The walk ends when the block being asked
about starts after the end of the period.
`LogChecksum` is N1MM's: a CRC32 over the `DXLOG` columns of the block, ordered
by time and then callsign, leaving out the columns each station works out for
itself — points, the three multiplier flags, the country prefix, the continent,
the WPX prefix and `IsOriginal`. Two stations are allowed to disagree about
those and still hold the same log. A checksum that disagrees for any other
reason costs one block of traffic, which is what a resync is for.
Every contact logged here now carries the computer that made it —
`NetBiosName`, which N1MM stamps and this program did not — because that is what
a resync asks for a block of contacts by.
A station that reconnects resynchronises by itself. N1MM sends `LASTQAT` — when
I last worked anybody — as soon as a connection opens, and a station that holds
nothing of that station's that late starts a resync from where its own copy
stops. An empty log asks for nothing: a program that has just been started has
no business pulling the whole contest across, and N1MM suppresses it the same
way.
**Rescore** writes the points and multiplier flags of a period back to the
database. The log in memory scores itself whenever it changes, so this is for
the stored rows: N1MM reads those columns rather than working them out, and a
contact that arrives from another station after the one that took a multiplier
leaves them behind. A station can also ask the others to rescore when its resync
finishes, which is the flag on the end of `CHECKSUM`.
What the link does not do yet: the log check that compares two stations' logs
row by row. It is a message type this program can already frame; it is not
written.
**Voice keying.** The keyer is written: a phone function key names a wav file,
`VoiceKeyer` plays it out of the sound card and keys the transmitter through
cwdaemon while it plays, and the voice keyer window lists every recording the
messages name, says which are there and records the rest. Alternating CQ duels
on phone as well as on CW, because the voice keyer says when a recording has
finished.
What is not written: N1MM's advanced voicing, which says a callsign or a serial
number by stringing together a recording per letter and per number out of
`LettersFiles`. That is why F5 sends nothing on phone, as it does in N1MM's own
default file. There is no DVK support either — the recordings are played on the
station's own sound card, so two radios transmitting at once share one card and
the second radio is what an SO2R station adds a box for. `{SETWAVDIR}`, which
points the folder somewhere else in the middle of a contest, is read and passed
over.
**The QTC window sends on all three modes.** On CW the header, the lines, the
QRV, the TU and the again messages go out through the keyer, with N1MM's
messages and N1MM's defaults.
On RTTY the station reading traffic out has Send All, which is N1MM's: the whole
series as one message, the heading, then every line with the operator's spacing
between them, then the ending, and `{TX}` and `{RX}` around the lot so it keys
the transmitter and drops it again. A line is the three fields joined with
hyphens, which is how N1MM writes them and it offers no setting for it. Snd n
sends one line again for a station that missed it. The heading, the ending and
the spacing are settings, N1MM's `WAESendAllHeadingText`,
`WAESendAllEndingText` and `WAESQTCSpacing`, with its defaults; `{ENTERLF}` in
them is new to the expander and stands for a carriage return and a line feed.
A whole series is the best part of a minute of transmission at 45 baud, which is
why it is one message rather than a button per line: the type-ahead buffer holds
it and paces the engine, and the operator presses one button. This has been
tested as text — what the message comes out as — and not on the air.
The station taking traffic down has N1MM's messages too — RX Ready, All Agn,
Agn n and Save, its `WAERXReadyText`, `WAEAllAgnText`, `WAEAGNText` and
`WAESaveQTCText` — and so has the station reading it out: R U QRV and Save,
`WAERUQRVText` and `WAESSaveQTCText`. All of them are settings in the window's
setup area, with N1MM's defaults. Clicking a line in the digital window fills
the next row, which is how a series is copied on RTTY.
On SSB the four buttons N1MM has recordings for play them — `WAEQRV.wav`,
`WAEAgain.wav`, `WAEConfirm.wav` and `WAETu.wav`, out of the `Wav` folder
rather than an operator's, which is where N1MM plays them from. The lines
themselves are read out by the operator: a line is a time, a callsign and a
serial number, and advanced voicing is what N1MM says those with.
What is not here: the green mark N1MM puts on each line as Send All reads it
out, which this sends as one message and cannot follow line by line.
**Cut numbers are not sent.** N1MM can key a serial number as letters — `N` for
9, `T` for 0 — and offers several styles. Nothing here does, in a QTC line or in
any other message.
**Two WAE numbers do not agree with N1MM, both from 2020 and 2021 logs.** The
disagreements are the country file of the day, not the rules: 4U1A counted as
`4U1V` then and as `OE` in today's `wl_cty.dat`, `KP2BH` counted as `KP2` then
and is listed as a US call now, and China was not split by call area yet. The
2022 to 2025 logs agree to the contact.
**Telnet window: what N1MM has and this does not.** Left out: the special-calls
list, and saving received spots to a database — N1MM keeps those spots in its
admin database, which is not the log file the two programs share, so there is
nothing to be compatible with and nothing asks to read them back. The band plan
tab is not repeated either, because Config ▸ Sub bands already edits the same
numbers.
The node list is downloaded from the public NG3K page rather than from N1MM's
own web service, which asks the operator to opt in to data collection and is
N1MM's to run. A page that changes shape would stop the download working; the
reading only needs `telnet://` links in a table, and the old list stays in place
when a download brings back something unreadable.
**Not every action macro is acted on.** The text macros and the action macros
that this program can carry out are listed in the README. What is read and
passed over: the `{CAT…}` and radio-hex families, the audio and rotator macros,
`{STEREOON}` and `{STEREOOFF}`, `{CONDJUMP}`, `{QSYCQ}`, `{FORCELOGNOTE}`,
`{COMMENT}`, `{SwapContests}`, `{VARYMSG1}` and `{VARYMSG2}`, the two
needed-QSO macros, and `{SETWAVDIR}`, which moves the voice keyer's folder in
the middle of a contest.
`{TIME1}`, `{DATE1}` and `{DAYTIME}` are passed over as well. They are the
Nordlink-TF, WA8DED and TAPR packet date formats, and there is nothing here to
check a guess at them against; the ordinary `{TIME}`, `{DATE}`, `{TIMEGMT}`,
`{DATEGMT}`, `{TIMESTAMP}` and `{TIME2}` are all filled in.
**Advanced voicing is not written.** N1MM says a callsign or a serial number by
playing a recording per letter and per number out of `LettersFiles` — `A.wav`
to `Z.wav`, `0.wav` to `19.wav`, the tens, `HUNDRED.wav`, `THOUSAND.wav`,
`STROKE.wav`, `QUERY.wav` and `POINT.wav`. Without it `!`, `#` and `@` in a
phone message have nothing to say, which is why F5 is empty in N1MM's own
default SSB file and in this program's.
A message holding any of them still sends the right characters.
The CW keyer macros are not read either: `<` and `>` for speed, `~` for a half
space, and the prosign characters `]`, `[`, `+` and `=`. They belong to the
keyer rather than to the expander — cwdaemon and a WinKeyer each have their own
way of saying them — and they go out as the characters they are.
**The busted-spot check is one character wide.** N1MM offers two. Every call one
character away from the spotted one is looked up in the callsign database, which
is a few hundred lookups per spot; two characters away is a few hundred thousand,
which is too much work per spot for the little it would add.
**Two BARTG contests share one set of rules.** N1MM has a class each for the
March HF RTTY contest, `BARTGSRTTY`, and the September sprint, `BARTGRTTYS`.
This program has one contest, named `BARTGRTTYS`, holding the March rules — the
ones the station's log needs — and both of N1MM's names open a log with it. A
sprint log would be scored by the wrong rules, and a log started here writes
`BARTGRTTYS` into the contest name, which N1MM would read as the sprint.
**Digital interfaces other than MMTTY.** A contact can be logged as RTTY or
another digital mode and the contest rules score it, and the digital window
decodes and transmits through MMTTY over the Wine bridge. What is not here:
fldigi, MMVARI, 2Tone and the hardware TNCs, all of which N1MM drives.
`DigitalEngine` is the place to add them.
**Contest coverage.** Twenty-six families are built in, plus whatever `.udc` files
are in the user-defined folder. N1MM ships well over a hundred. Opening a log
from a contest that is in neither place fails with the contest name, which is
the right answer but it is still a wall.
Most of what is missing does not need code. A contest whose exchange is a
report and one value, scored by band, mode, continent or country, and counted
by country, zone, section or prefix, is a `.udc` file, and the file published
for N1MM is read as it is. Code is for the ones `.udc` has no words for: WAE
needs QTC traffic, IOTA needs island references as multipliers, and CQ WW RTTY
needed a third multiplier and its own points table because it shares a name
with the CW and SSB running of the contest.
`.udc` settings that are still passed over. `MultiplierBands` and
`QsoErrorString`, which are about the windows rather than the score.
`GenericPrintString`, which is the layout of a printed log rather than a
Cabrillo one.
`MultWindowType` is read as far as the score goes: it names the list a section
multiplier is checked against. Four of N1MM's lists are held here — the ARRL
sections, the US states, the Canadian provinces and the OK/OM districts — and a
file naming any of the hundred-odd others falls back to counting any exchange
that is not all digits.
`CabrilloString` and `CabrilloVersion` are read: a contest whose sponsor asks
for its own columns gets them, in N1MM's shape — the operator's callsign in the
first 13 columns, then every column padded to the width the file gives with one
space after it, and a value longer than its column cut rather than pushed. The
numbered `CabrilloFormat` layouts 2, 3, 4 and 5 — the NAQP, NA Sprint,
Sweepstakes and section-and-serial lines — are read as well, in N1MM's columns.
The exchange those layouts split into columns is built from the boxes the file
defines, in the order it defines them; N1MM builds it from the section box
alone, which writes only half of a two-box exchange. Layout 6, the ARRL RTTY
Roundup line, is not read, and a file asking for it gets the default line; no
published `.udc` file asks for it, because the Roundup has a contest class of
its own. `CabrilloFormat = 0` means the sponsor takes no Cabrillo log at all,
and nothing here stops the operator writing one.
N1MM's `.udc` format has no way to say that the exchange itself differs by the
other station's country. OK/OM DX is written in code for that reason — it asks
a county from OK and OM stations and a serial number from everyone else — and
`Contest.SkipsField` steps over the box that does not apply. REF, ARRL 10M, the
Ukrainian DX contest and the Russian DX RTTY contest ask their exchange the same
way, and are written in code for the same reason.
Every contest in the station's own logs can now be opened. The replay of
2026-09-06 read 62 of its instances with built-in rules, 55,415 contacts, and 45
of those instances scored contact for contact as N1MM did; 316 contacts differ
in points and 141 in a multiplier flag. The comparison is against the points and
flags N1MM left in the file, which may themselves be stale, so a difference is
something to look at rather than a fault. The counts and the row-by-row list are
in [scoring-audit-2026-09-06.md](scoring-audit-2026-09-06.md) and
[scoring-audit-history.csv](scoring-audit-history.csv).
What still differs, and why. The IARU, YOTA and CQ WW differences are not in
this list: they are in the audit, and each needs the station settings and the
country file of the day before it can be said what happened.
- **The EU DX Contest** is written from N1MM's own class. The station's logs
were made with the `EU_DXC.udc` file instead, which scores a contact inside
your own country 1 point where the class scores 2, and counts a district code
once in the contest where the class counts it once per band.
- **Field Day Region 1**: every contact in that log holds 1 point and no
multiplier, which is not what N1MM's own class works out, so the log looks
like it was never scored. The rules here follow the class: 5 points for a
portable station, 2 for one in Region 1 at home and 3 from outside it. That
table is applied to every running of the contest, which finding 12 of the
audit says is wrong: N1MM branches on the running and on the entrant's
country, and continent alone does not say whether a station is in Region 1.
- **SA 10 and OK/OM DX on SSB** are not written in code at all: both are
published as `.udc` files, and with those files in the user-defined contests
folder the station's logs score to the contact. N1MM's OK/OM DX class is the CW running only and says so; the SSB
running has different rules and its own pair of files, one for each side of
the contest.
- **Two OK/OM DX contacts** whose district code was stored with a trailing
space. N1MM checks the code against its list without trimming it, finds
nothing and counts no multiplier; the code is trimmed here and counts. Both
contacts are the same station in the 2026 log.
**The statistics window: what N1MM has and this does not.** The window counts
the log under any two of its fields, in N1MM's own two lists — 29 fields down
the rows, 20 of them across the columns — with N1MM's `Tot` and `Accum` columns,
its `Total` line, its "Include Dupes & X-QSOs" box and its stacked column graph.
Copy All takes whichever tab is open, as N1MM's does: the table as spaced text
off the table tab, a picture of the graph off the graph tab. File ▸ Copy Table
as Text and File ▸ Copy Graph as Image do either from either tab, and File ▸
Save Graph writes the picture to a PNG. Left out: printing to a printer, which
needs a print dialog this program has nowhere else, and N1MM's font size
spinner, because the theme dialog sets the fonts here.
X11 hands over a picture only when the other program pastes, not when it is
copied, so `StatisticsWindow` holds the bitmap it gave the clipboard until it
gives it a different one. Disposing it at the end of the copy killed the
program on the next paste.
The palette names are N1MM's twelve, which are the .NET chart control's. The
colours are not: they are chosen to match the name, because the control's own
values are not published anywhere this could read them from. Berry is purple
through magenta, Fire is red through yellow, and so on.
**The statistics graph is drawn here rather than by a chart library.**
`StatisticsChart` draws the axis, the stacked columns, the labels and the
legend. It has no tooltips, no zoom and no 3-D, all of which N1MM's chart
control gives it for free. A column label that will not fit under its column is
turned on its side, and one that will not fit even then is left out.
## Broadcast data
What the Broadcast Data dialog saves but does not send yet, or sends in less
detail than N1MM:
- **Rotor.** The row is saved. Nothing sends `N1MMRotor` messages, because this
program has no rotor control.
- **Spots.** Only `add` goes out. N1MM also sends `delete` when a spot leaves the
bandmap. Spots that expire are not reported.
- **Score.** The rows, their order, the band and mode labels and the layout
follow N1MM's query and writer. The row set and order were checked against the
query on `om5m.s3db` for 62 contests. Points and multiplier flags come from our
own scoring, so they carry the differences listed in `n1mm-interop.md`. The
multiplier types come from a table of N1MM's names (`N1mmMultiplierNames`)
for the built-in contests; a user-defined contest uses the names its rules
give. N1MM writes text values unescaped and we escape them. The bands' wavelength
labels were read from N1MM's code and not compared with its output.
- **Radio.** `RadioName`, `Antenna`, `Rotors`, `FocusEntry`, `EntryWindowHwnd`
and the aux antenna fields are sent as 0 or empty, and `IsTransmitting` is
always `False`.
- **Contacts, All Computers.** Only logged contacts are passed on, not edits or
deletes.
## Radio mirror
Config ▸ Radio Mirror copies the frequency, mode and split of a radio on another
computer, for a station whose radio port is used up. The listener applies
`RadioInfo` to the entry position of one local radio and commands no radio. Nothing is checked against a real N1MM or two
real stations; the test sends our own message over loopback. A mode name other
than `CW-R` and `FSK` that this program has no mode for leaves the mode
unchanged. `IsTransmitting` is read but not used.
## Known rough edges
**The band plan covers 160M to 2M only.** 60M and everything above 2M get no
CW, digital or phone shading on the bandmap, because N1MM's own numbers for
those bands contradict themselves — its CW top for 1.25M is below its phone
start, and its 70CM CW top is above the band. Config ▸ Sub bands cannot add a
band that has no default.
**Available Mults and Qs differs from N1MM's window.** Reference:
https://n1mmwp.hamdocs.com/manual-windows/available-mults-and-qs-window/
Needs SO2R, which has two radio positions and two entry windows already:
- the window judges and tunes with the first radio's position only;
- Focus Radio, Non-Focus Radio and Only Radio 1/2 filters;
- Shift-click sends a spot to the other radio;
- Spot to Associated Radio, with the warning when the window opens;
- Focus Follows Spot, which moves focus to the radio the spot went to;
- a second Available window, one per radio;
- Enable Right-click QSY on Spot, which sends the spot to the non-focus radio;
- Alt+B, Shift+Alt+B and Ctrl+Alt+B for the second window.
Needs SO2V, which is not implemented at all (the program has split transmit
through rigctld `S 1 VFOB` and a bandmap bar for the second radio, nothing else):
- reading the sub-receiver or VFO B from the rig, and a position for it;
- Basic and Advanced SO2V VFO control, where a spot goes to the VFO already on
its band, otherwise the active VFO (Basic always uses VFO A);
- the Basic or Advanced label in the title bar.
Needs something the program does not have:
- the ♥ column, because there is no Special Calls list in the telnet window;
- the heard column, because there is no spectrum display;
- Turn Rotor, because there is no rotator control;
- the `(!)` marker for a local Skimmer, because a spot does not say whether the
Skimmer is ours.
Smaller differences:
- the TS header does not change colour while QSY after logging is on;
- the last picked line has the grid's own selection, not N1MM's light green
frame;
- a left click does both the QSY and the call load, where N1MM loads the call
only on a double click;
- the title bar shows the Qs, Mults and total counts, where N1MM also shows the
VFO or focus mode;
- there is no option to turn off the Pts column's calculation to save CPU;
- tooltips are always on, not tied to a setting;
- the column order is not kept between runs, only which columns are hidden.
**Available Mults and Qs is only as good as the bandmap.** It lists what is
spotted, so a multiplier nobody has spotted is not there. N1MM's window also
shows the mults a contest has and nobody has worked, which needs a list of every
multiplier the contest counts. Only the section and state lists are held here,
so the zone and country lists would have to come from the country file first.
**Two entry windows and one keyer.** The SO2R box is told which radio to key
before each message, but nothing stops both radios sending at once if the
operator asks them to.