Start the transmit pane again on every new message

The macro buttons went wrong at the end of a transmission, both faults in
DigitalEngineSender:

- The engine reports the transmitter drop on its own thread.
  WhenTransmitChanged took the state lock, decided the message had ended,
  released the lock, and only then called Buffer.Ended(), which clears
  what has gone to the engine. A macro pressed on the last character got
  through StartAsync in that gap and had already flushed its own text into
  Sent, so Ended() wiped the new text off the pane while the engine
  transmitted it. Ended() is now called inside the same lock.

- The pane only started again when the engine reported a drop. Two macros
  in a row keep the transmitter up, so that report never came and Sent
  grew with every press. Everything in Sent is locked, because it is in
  the engine and cannot be taken back, so the whole pane became
  read-only. TypeAhead.Started() drops the last message's sent text and
  keeps what was typed ahead, and StartAsync calls it whenever it keys a
  new transmission.

The rest of this commit is the digital transmit work these fixes sit on:
the pane as one coloured box, the sender's three keying states, the
type-ahead feeder paced by the clock with the engine's count as a brake,
{RX} flushing what is left in one piece, and the entry window's function
keys reading the digital macros on a digital mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
This commit is contained in:
2026-09-03 14:40:30 +00:00
parent 7ae60be4a0
commit f49a8c10fd
20 changed files with 2604 additions and 390 deletions

View File

@@ -44,37 +44,218 @@ 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.
**What paces the type-ahead pump.** The text is held in `TypeAhead` and the
engine is kept `Lead` characters ahead: one being transmitted and one behind it,
so it never runs dry and never transmits idle in the middle of a message. Those
two characters cannot be taken back; everything behind them can still be
rewritten.
**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.
The pace comes from the clock at the baud rate in the digital settings. The
engine's own count of what it has left is a check on that rather than the pace
itself, for the reason in the table below: it is used as a cap, so the engine is
never given more than `Lead + Slack` characters however fast the clock says to
feed, and as the end of a message, together with the clock estimate. A count
that neither goes down nor is being added to means the engine is holding what it
has, and the pump feeds a character every `HoldingPatience` until it moves.
**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.
`{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. `TypeAhead.WhenHoldingAsync` sends the stop as soon as the count
says the engine holds something, and gives up after N1MM's 400 ms.
So `{RX}` stops feeding: `TypeAhead.TakePending` 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.
`TypeAhead.WhenEmptyAsync` 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:
| What | Answer |
|---|---|
| Does the control answer `TxBufLen`? | Yes, and a name it does not know fails with `DISP_E_UNKNOWNNAME`, so the two are distinguishable. |
| What does it count? | Characters left to transmit. Seventeen were pushed; a second later, with five decoded back off the air, it read 12, and it counted down to 0 as the message went out. |
| How current is that number? | It lags by 100 to 150 ms, about one character at 45.45 baud. Twenty-one characters pushed at 2496 ms read 0 at 2497, 2547 and 2597, then 26 at 2647. That is why the count cannot be the pace: a pump that fed on a fresh 0 would hand over a whole message in half a second and put all of it beyond reach. |
| What is it counting? | Baudot symbols, not the characters handed over. Twenty-one characters read as 26, and the message was `CQ TEST DE OM5M OM5M `: two digits, each needing a shift to figures and a shift back, plus one shift at the start. It also only moves once per symbol transmitted, so four readings 50 ms apart are the same number. |
| Does a backspace take back a character the engine has not transmitted? | No. Pushed in as a character it made the count go **up** by four, and the text went out unchanged. MMTTY's help describes its backspace key doing this in its own window; through `PostMmttyMessage(4, 8)` it is just another character. So text in the engine cannot be retracted, only aborted. |
| Way to send | This engine is set to Character out: `ABCD` with no space after it went out at once. MMTTY's help says Word out is the usual setting, which would hold that word, so the pump copes with both. |
| Does the count reaching 0 cut the end of a message off? | No. It reached 0 about 350 ms before the last character was decoded back, and the engine held the transmitter up for another 900 ms after that, so `{RX}` straight after the count empties does not truncate anything. |
Counting symbols rather than characters is what makes the cap earn its place. A
contest exchange is full of digits, so the engine has more to transmit than the
pump's clock thinks: `599 001` is seven characters and eleven symbols. The clock
would run ahead of the engine on every exchange; the cap is what stops it.
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,
@@ -82,6 +263,14 @@ 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
@@ -90,6 +279,51 @@ arrives about 420 ms after the character goes out and matches what was sent.
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. N1MM does not read that report at all: it binds an
empty handler to `OnPttEvent` and takes the transmit state from MMTTY's window
message 32772 instead, which the bridge does not forward yet.
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.
**Voice keying.** `MessageSender` was written to cover a voice keyer playing a
recording, and nothing implements it. Each operator's recordings folder is
stored with the rest of his settings and waits on the same thing. No DVK support either, so the second radio