781 lines
48 KiB
Markdown
781 lines
48 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 `ADDSPOT` and `PACKET` are not written.
|
|
|
|
**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.
|
|
|
|
## 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 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.
|