Alternate CQ between the two radios
Ctrl+B calls CQ on one radio, and when that message has gone out, moves to the other and calls there. The keyboard, the entry window and the SO2R box follow each turn. N1MM calls this dueling CQs and puts it on the same key. The turn is taken when the keyer says the message has ended, not when a timer guesses it has. So MessageSender grew a Finished event and a ReportsCompletion flag, and both keyers fill them in: cwdaemon answers the <ESC>h reply request that now goes out in front of every message, and a WinKeyer clears the busy bit in the status bytes it sends of its own accord. The status-byte reading is in WinkeyerStatus, away from the serial port, because that is the half that can be tested without a keyer on the desk. A keyer that reports nothing refuses to start alternating CQ rather than keying the second radio over the first. AlternatingCq itself takes the keyer, a callback that calls CQ on a radio, the gap and a wait function, so the alternation is tested without sleeping. The gap is in Config ▸ Keyer and messages and will not go below 100 ms, which is N1MM's floor too: an SO2R box works relays. docs/keying.md writes down why cwdaemon does the timing and we do not. N1MM keys DTR itself with a coarse sleep, a busy-wait and a margin that grows every time the sleep overshoots, and it raises the thread to TIME_CRITICAL for the length of the message. The busy-wait ports to Linux; the priority does not, without CAP_SYS_NICE, and a garbage collection mid-element is audible. A direct serial keyer stays a reasonable third option, to be taken knowingly. Running it against a fake daemon that takes 1.5 seconds to play a message: six CQs went out back to back and the keyboard moved between the two entry windows each time. Escape stopped it, let the message in flight finish, and started nothing further. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
87
docs/keying.md
Normal file
87
docs/keying.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# How CW gets on the air, and why
|
||||
|
||||
Written 2026-08-27.
|
||||
|
||||
Nonemm has two keying paths and neither of them times the Morse itself:
|
||||
|
||||
| Path | What it is |
|
||||
|---|---|
|
||||
| `CwDaemonSender` | UDP to `cwdaemon`, which keys DTR or RTS on a serial port and does the element timing |
|
||||
| `WinkeyerSender` | serial to a WinKeyer in host mode, which does the element timing in hardware |
|
||||
|
||||
Keying the serial port ourselves — writing the dots and dashes from inside the
|
||||
logger — was looked at and left out. This file says what that would take and
|
||||
why the answer was no for now.
|
||||
|
||||
## What N1MM does
|
||||
|
||||
N1MM has no cwdaemon. It keys the port itself, in `CWInt.cs`:
|
||||
|
||||
- `PortOn(n)` sets `CommPort.DtrEnable` or `RtsEnable` (or writes a parallel
|
||||
port bit through `inpout32.dll`), then waits `1200000 × n ÷ wpm`
|
||||
microseconds, minus the time the port write itself took. `PortOff(n)` is the
|
||||
same with the line dropped. `n` is the element length in dot units.
|
||||
- The wait is `waitunit`: sleep in 40, 20, 4 and 1 ms steps while there is
|
||||
slack, then busy-wait on a `Stopwatch` for the rest. An element can end late
|
||||
but never early.
|
||||
- The spin margin measures the machine. `CntDnAmount` starts at 2000 µs and
|
||||
grows every time a sleep overshoots — 200 µs for a small overshoot, up to
|
||||
10 ms for a large one — and never shrinks during the run. After a few
|
||||
characters it has found how sloppy this machine's timers are and starts
|
||||
spinning early enough to land on time.
|
||||
- Error does not accumulate: each `PortOn`/`PortOff` starts its own
|
||||
`Stopwatch`, so one late element does not push the rest late.
|
||||
- `sendCW` raises the thread to `THREAD_PRIORITY_TIME_CRITICAL` for the length
|
||||
of the message — `SetPriority((IntPtr)32, 15)` — and drops it back to normal
|
||||
afterwards. While CW is going out, that thread preempts the screen, the
|
||||
database and the network.
|
||||
|
||||
The keying also sits behind its own UDP listener, `CWIFMain` and `UDPClass`,
|
||||
which N1MM calls the CW interface. In N1MM Classic it was a separate process;
|
||||
in Logger+ it is a module in the same process, still spoken to over UDP. That
|
||||
is the same shape as cwdaemon.
|
||||
|
||||
## Why we use cwdaemon instead
|
||||
|
||||
Most of N1MM's recipe ports. `SerialPort.DtrEnable` works on Linux, `Stopwatch`
|
||||
is the same class, and a thread can spin the same way. Two things do not:
|
||||
|
||||
**Thread priority.** `THREAD_PRIORITY_TIME_CRITICAL` has real teeth on Windows.
|
||||
On Linux, .NET's `ThreadPriority.Highest` is a nice value, and nice does not
|
||||
stop the scheduler taking the core away mid-element. The equivalent is
|
||||
`SCHED_FIFO`, which needs `CAP_SYS_NICE` or root. cwdaemon can have that
|
||||
privilege; a logger the operator starts from a desktop should not ask for it.
|
||||
|
||||
**Garbage collection.** A collection that stops the keying thread part way
|
||||
through an element makes an element the wrong length, and that is audible.
|
||||
N1MM has the same exposure and lives with it. cwdaemon does not have it at all,
|
||||
being C.
|
||||
|
||||
The parallel port is not worth copying either: `inpout32` has no Linux
|
||||
equivalent that works without root, and the hardware is gone.
|
||||
|
||||
## What was decided
|
||||
|
||||
Keep cwdaemon as the Linux path and the WinKeyer as the hardware path. A
|
||||
`SerialCwSender` doing N1MM's coarse-sleep-then-spin is a reasonable third
|
||||
keyer kind later: it would remove the install-cwdaemon step, it is the only
|
||||
software path that works on Windows without a WinKeyer, and its completion
|
||||
signal would be exact rather than a UDP round trip. On Windows it would be as
|
||||
good as N1MM. On Linux it would be worse than cwdaemon, for the two reasons
|
||||
above, and that is the trade to make knowingly rather than by accident.
|
||||
|
||||
## Knowing when a message has gone out
|
||||
|
||||
Alternating CQ needs the end of a message, and both paths report it:
|
||||
|
||||
- cwdaemon: `<ESC>h<text>` in front of the message asks for a reply, and the
|
||||
daemon sends `h<text>` back on the same socket once it has played. The
|
||||
request covers one message, so it goes out before every message.
|
||||
- WinKeyer: a byte from 0xC0 to 0xDF is a status byte and 0x04 is set while the
|
||||
keyer is sending, so busy going off is the end. The bit meanings are from
|
||||
N1MM's `Winkey.cs`; the K1EL datasheet is a scanned PDF that does not extract
|
||||
as text.
|
||||
|
||||
Timing the message from the length of its text was rejected. The guess runs
|
||||
short exactly when the operator has turned the speed up, and a short guess keys
|
||||
the second radio while the first is still sending.
|
||||
@@ -16,22 +16,22 @@ is still sitting there undiscovered.
|
||||
| `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. |
|
||||
| `CwDaemonSender` | the UDP messages | no `cwdaemon`, no radio keyed. |
|
||||
| `WinkeyerSender` | nothing | no test at all, and no WinKeyer. The host-mode open sequence is from the WinKeyer datasheet. |
|
||||
| `CwDaemonSender` | the UDP messages, and a fake daemon that answers the `<ESC>h` reply request | no `cwdaemon`, no radio keyed. |
|
||||
| `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`. |
|
||||
|
||||
The Cabrillo output has not been put in front of a contest sponsor's robot.
|
||||
|
||||
## Half-built
|
||||
|
||||
**Alternating CQ (SO2R).** Needs to know when the keyer has finished sending.
|
||||
`MessageSender` is fire-and-forget: it has `SendAsync`, `AbortAsync` and
|
||||
`SetSpeedAsync` and no completion signal. Both `cwdaemon` and a WinKeyer can
|
||||
report completion, so the interface has to grow first. Timing it from the length
|
||||
of the text would be a guess that goes wrong exactly when the contest is busy.
|
||||
|
||||
**Voice keying.** `MessageSender` was written to cover a voice keyer playing a
|
||||
recording, and nothing implements it. No DVK support either, so the second radio
|
||||
of an SO2R station cannot call CQ by voice.
|
||||
of an SO2R station cannot call CQ by voice. Alternating CQ therefore works on CW
|
||||
only, though nothing in it is CW-specific: a voice keyer that reports when the
|
||||
recording has finished would drive it as it stands.
|
||||
|
||||
**Alternating CQ does not restart itself after a contact.** Escape stops it, and
|
||||
it has to be started again with Ctrl+B. N1MM carries on calling after the QSO is
|
||||
logged.
|
||||
|
||||
**Call history: the section-validating directives.** `!!Validate50State!!`,
|
||||
`!!ValidateArrlSection!!`, `!!MapOnSection!!` and `!!GTA2GH_NT2TER!!` are read
|
||||
|
||||
Reference in New Issue
Block a user