Files
Nonemm/docs/keying.md
T
erikbrockoandClaude Opus 5 082fa75a40 Play a recording on phone, and record it in a window
A phone function key names a wav file instead of words, which is what N1MM's
own SSB file holds: `F1 CQ,{WAVDIR}\Cq.wav`. `{WAVDIR}` is the folder named
after whoever is at the microphone, a name is taken relative to `Wav`, and
commas play several files in a row. An SSB `.mc` file written for N1MM plays
here.

Window ▸ Voice Keyer lists every recording the messages name, says which of
them are there for the operator picked at the top, plays one, and records over
one. The recording is written beside the wanted file and moved over it at the
end, so a recorder that will not start takes nothing away.

`CommandSoundCard` runs `aplay` and `arecord`, and both commands are settings:
the audio path belongs to whatever the station runs, and a library would pick
one of them for the operator. A recorder is stopped with an interrupt so it
closes its wav file, and killed only if it ignores that. PTT is cwdaemon's
`<ESC>a` for as long as the recording plays, off for a station on VOX.

Two things follow. The QTC window sends on SSB: N1MM's four recordings on the
Ready, Agn, Cfm and Close buttons, with the lines still read out by the
operator. And alternating CQ duels on phone, because the voice keyer says when
a recording has finished — which it had to, since the CQ message is picked by
the radio's mode and would otherwise have been keyed as Morse.

Nothing here has been played on a radio: this machine has no sound card, so
what is tested is the program being run, the file filled in, the exit code and
the interrupt, with shell commands standing in for the two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-11 07:51:58 +00:00

6.4 KiB
Raw Blame History

How CW gets on the air, and why

Written 2026-08-27. The voice keyer was added 2026-09-11.

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.

The voice keyer

A phone message is a recording rather than characters, so nothing has to be timed: the file is played, and the transmitter stays keyed until it has been. Two decisions are worth writing down.

The sound card is a program, not a library. CommandSoundCard runs aplay and arecord, and both commands are settings. The reason is the reason above: the audio path belongs to whatever the station runs — ALSA, PulseAudio, PipeWire, JACK — and a library picks one of them for the operator. A station that routes its audio some other way writes its own command with {file} in it, and a station on Windows, which has no player on the path, gets a PowerShell line that plays through .NET's own SoundPlayer.

The cost is a process per message. It is a few milliseconds at the front of a recording that runs for seconds, and no part of a voice message needs the timing that CW needs.

A recorder is stopped with an interrupt, not a kill. arecord closes the wav file and writes its header when it is interrupted; killed outright it leaves a file with a header that says nothing was recorded, which nothing can play. So the stop is SIGINT, and the kill is only what follows two seconds later for a command that ignores it. The recording is written beside the wanted file and moved over it at the end, so a recorder that will not start takes nothing away.

PTT is cwdaemon's <ESC>a, which keys the transmitter and leaves it keyed. That is the only way into the keying port from here, so a station with a WinKeyer, or with no CW keyer at all, runs the voice keyer on VOX. The <ESC>b request, which tells cwdaemon whether the SSB audio comes from the microphone or the sound card, is not sent: what it switches is the daemon's own wiring, and a station that needs it has already set it.

None of this has been run against a radio. This machine has no sound card, so what has been exercised is the program being run, the file being filled in, the exit code and the interrupt — with ordinary shell commands standing in for aplay and arecord.