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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user