Skip to content

RTTY Stack - #4

Merged
kb10uy merged 41 commits into
masterfrom
rtty
Sep 13, 2026
Merged

kb10uy merged 41 commits into
masterfrom
rtty

Conversation

@kb10uy

@kb10uy kb10uy commented Aug 14, 2026

Copy link
Copy Markdown
Owner

No description provided.

kb10uy and others added 27 commits August 15, 2026 07:07
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The comparator's own two channel outputs had nowhere to go: `FrontEndOutput`
is crate-private and only its `|mark − space|` magnitude reached the squelch.
`rx/monitor.rs` names the pair `ChannelLevels` and hands it out two ways —
`ReceivePipeline::channels` for the newest one, which is the signed tuning
figure a receive column's header reads, and a decimated bounded ring behind
`set_monitor`/`drain_monitor` for an XY scope to draw.

Both are settings rather than construction arguments: a scope is opened and
closed while a reception runs, and rebuilding the pipeline to open one would
throw away the reception being watched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The application is started from apps/wefax, as the memo records: main.rs, the
menu renderers, the identity, the locales machinery, the configuration
document, the paths, and the WAV source are that application's, and what is
new is the receive path itself.

One `ReceivePipeline` per column, every one of them fed the same PCM, with a
header carrying what that path is hearing — the pair after any AFC movement,
which tone the comparator is on, the case, and the signal reading. There is
one decode path because the core has one demodulator; `DecodePath` is the
list the columns are built from, so a second discriminator is a variant and a
pipeline rather than a layout.

A receiver cannot be retuned in place, so a settings change rebuilds the
paths. That costs a few milliseconds of filter settling and nothing else: the
text lives in the interface.

The pane collapses a run of carriage returns and line feeds into one break,
because a teleprinter line ends with CR CR LF and honouring each of them
would double-space everything.

Transmit, the scope window, and the macro engine are the milestones after
this one and are not here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three touch-points a new application drags with it, each hard-coded per
application: the core job's exclusion list and a third leg of the app matrix
in ci.yml, an rtty case in package/build-app.sh with its own microphone
sentence, and release-rtty.yml on the rtty-v* tag pattern.

Automatic frequency control now starts off in the application. Running it
against a real capture device is what found this: on an empty band the search
takes the two strongest peaks it can find, which are the receiver's own hum
near the 300 Hz floor, and they snap to a 170 Hz shift as readily as a
transmission does — a watch left running walks off the pair it was put on.
The demodulator squelch cannot gate the correction, because a pair far enough
off to need AFC is exactly the pair that fails to open it; that is written
down in rtty.md along with what the real fix would be.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fourth step of the RTTY application's plan: a window the operator opens
from the panel beside the text or from the View menu, showing the band the
receiver is listening across and the two channels it is comparing. It is a
deferred viewport with the always-on-top flag, because it is read while the
panel behind it is worked and because the main window sleeps between the
frames a reception gives it.

The band is transformed in the receive worker rather than in the interface.
That departs from the plan and not from its reasons: the raw PCM is already
there, the publish interval is the thirty-a-second throttle the plan asked
for, and a closed window then costs nothing whatever, because neither the
transform nor the samples it reads exist while it is closed. It is 2048
points of its own, deliberately not the frequency control's, and it is
published to 4000 Hz — MMTTY's wider display ceiling, which is what a 3000 Hz
mark and an 850 Hz shift need.

The channel pairs come from the monitor tap the core grew for this. The tap is
opened through a control of the worker's own rather than through
WorkerSettings, because a settings change rebuilds every pipeline and opening
a window must not throw away the reception being watched. Only the first
decode path is tapped, every column being fed the same audio, and the pairs
are decimated to about four thousand a second, which is more than the width of
the picture they are drawn into.

Two things the picture does that the plan did not say. The trace is scaled by
the strongest pair on it, because a channel carries a rectified envelope whose
height depends on how much of the band-pass the tones fill, and what is read
off the picture is the shape rather than the level. And the line where the two
channels are equal is drawn under it, because that line is the comparator's
own decision.

Every frame carries a sequence number: nothing on a scope is compared against
a threshold, so a trace that looks like the last one is still a new trace, and
without it the interface's own change comparison would sleep through a band
that had not moved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The application needs to underline text as it is keyed, echo it into the
received text a character at a time, report how long is left, and hand back
whatever an aborted transmission did not send. All four want the same figure
and none of them can derive it: the encoder inserts a shift wherever the case
changes and a carriage return before a bare line feed, so codes outnumber
characters, and the lead-in idles before any of them.

TxSchedule walks the encoder the transmitter is fed from and accumulates
per-code durations, reporting the sample position at which each character of
the source text finishes. Timing -- samples per bit, the length of one framed
character, and the lead-in, tail, and ramp -- moves out of Transmitter so both
are laid out from the same arithmetic; a test asserts the scheduled length
against what the transmitter actually produces across gap, parity, stop, and
lead-in variations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Transmission is buffered: a message is written in a field along the bottom,
Ctrl+Enter queues it, and it goes out when whatever is ahead of it has
finished. Enter writes a line, the way it does on a teleprinter, so reaching
for a new line cannot put half a message on the air. Stop cuts the carrier
where it was pressed and gives the unsent remainder back to the field, because
an aborted call is finished by editing it and sending again.

The field only ever holds what can be sent. A keystroke with no Baudot code
simply does not appear, which is the answer a teleprinter with no such key
gives; pasted and expanded text keeps what it cannot send and draws it in red,
because a block that silently lost part of itself would hide the loss. Both
paths upper case what they take, so the field shows what leaves.

What is sent is printed into the received text in a colour of its own,
following the playback position rather than the generated audio, so the
transcript reads as the band heard it and a stopped transmission says what
actually went out. The same positions drive the underline on the message being
keyed and the time remaining.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Macros carry what an exchange repeats: the callsigns, the name, the report,
the date. MMTTY's ConvString is the reference for what they can say but not
for how they say it. Percent letters are replaced by the ${...} names the
family's other application already interpolates its templates with, so an
operator who has written one has written both, and a misspelled name survives
the expansion into the field where its braces are refused and the send button
is held -- the mistake stops where it can be seen.

The buttons answer to F1 through F12. A press writes the message in at the
caret rather than sending it, so what is about to go out can still be read and
edited; a macro marked to send in the configuration goes out as a message of
its own and leaves a half-written reply alone. Definitions live in the
configuration, written once on first run and then left alone, because the file
is where they are edited and rewriting the array on every save would reformat
what the operator had put in it.

The fields the macros read are beside the received text, where they are filled
in from: a callsign is double-clicked out of the line that printed it, and a
report is judged against the meter above it. They run the same keyboard filter
the message field does, since what is typed into them is typed to be sent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A message leaves the queue before its playback stream is opened, so a stream
that would not open dropped it: the operator was told the device was missing,
but not that the text they had written went with it.

What could not be started now comes back to the message field along with
everything queued behind it, the same way stopping a transmission does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The implementation memo described transmit as unwritten, and the four places
the interface departed from it are worth the record: Enter writing a line
rather than sending, the field that only holds what can go on the air, the
sent text printed into the received text, and the unsent remainder coming back
to the operator.

The macros departed further. The placeholders are the ${...} names apps/sstv
already interpolates its templates with rather than a second convention
invented here, an unknown name is left where it was written so the field's own
red marking reports it, and no macro editor was built because there is nothing
left for one to validate. The memo's claim that the dollar sign is outside
ITA2 was wrong, which matters only in that it is FIGS-D and so sendable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Several test literals carried an actual line break where the escape was meant.
They compiled to the same bytes, so nothing was wrong with what they asserted,
but a string split across four lines to hold one newline is unreadable and
rustfmt had nothing to bring it back to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A callsign, a name, and a QTH are entered when the application is first set up
and then left for years, while everything else on the panel beside the text is
worked at every exchange. Having them share a panel made the one that is never
touched as prominent as the ones that always are.

They move into a modal window on the Settings menu, which is where the SSTV
application already keeps the same three. The contact fields stay on the panel,
because those are filled in from the line that just printed. The window runs
the same keyboard filter the message field does, since what is typed into it
reaches the air through the macros that read it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A station says things about itself that no application can know to ask for: a
grid square, a club, a rig. The SSTV application already lets those be named
and read back from a template, and RTTY macros want the same.

They are edited in the window that already names the station, rather than in a
second dialog: the list is short and everything in it is the same kind of
thing, something this station signs with. A field is reached from a macro as
${custom.<name>}, and the prefix is what keeps the operator's names apart from
the built-in ones, so adding a built-in later cannot take a name out from
under a macro already using it.

A name is taken up when the field is left rather than on every keystroke,
because half a name is a different field, and one no expression could hold is
marked and kept out of what the macros see while staying on screen to be
fixed. The value runs the sendable-character filter and the name does not: the
value goes on the air, the name never leaves the configuration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kb10uy and others added 12 commits September 13, 2026 11:56
The RTTY macros interpolate the same expressions the SSTV templates do, and
had grown a second reader for them: one that understood the braces but not the
$$ escape or the :format suffix, and that answered an unknown name by leaving
it where it stood. Two readers of one syntax drift, and an operator who has
written a template should have written a macro.

grayline-variables now holds the expression syntax, the rule for what may be
named, and the values a name can stand for. What the names are stays with
whoever is being written for: this crate has no opinion about callsigns.

grayline-sstv-template keeps its public API by re-exporting the three items
that moved, and its MissingVariable and VariableFormat variants become one
Variable variant carrying the new crate's error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The macros understood the braces and nothing else: no $$ escape, no :format
suffix, and an unknown name left where it stood. They now go through
grayline-variables, so all three behave the way a template does. A name
nothing was provided for is refused and reported by name, rather than putting
a message with a hole where a callsign belongs into the field.

The names are the templates' too. The two RSTs become report.sent and
report.received, the date and time become one tx.timestamp.utc that a macro
formats however it likes, and station.grid and application.version arrive with
them. Three of SSTV's are missing because this application has nothing to
answer with -- radio.* wants rig control, rx.timestamp.* wants a reception
with a beginning and an end, and report.number wants the contest serial that
was deferred. One is added: greeting, which is MMTTY's and which no template
ever wanted.

contact.name keeps MMTTY's OM fallback. That is not the kind of placeholder
the templates refuse, which is the word Name standing in for a name; OM is
what an operator actually says to a station whose name they have not been told.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
U+23CE is the return symbol, and a face that has no drawing for it puts a box
in the middle of the message it was meant to punctuate. U+21B5 is the older
character and the one a text face is far likelier to carry.

The mark was written out at all three places that draw a message on one line;
it is now named once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The mark a transmit message shows a line ending with was never drawn by the
coding font. Neither Consolas, Courier New, nor Segoe UI carries U+21B5, so on
Windows it fell through the whole monospace chain to one of egui's bundled
faces, which are installed with a scale tweak of 0.81 and 0.90 -- the arrow
was drawn smaller than the text around it and sat off the underline it was
supposed to sit on.

Monaspace has the character and now leads the list on every platform. It is
the one family there an operator installs on purpose rather than finds already
present, and the one that covers what a teleprinter transcript gets annotated
with.

A machine without it still falls through to the bundled faces. That is worth
knowing rather than working around: the alternative is a mark chosen for the
poorest font on the list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The window was written against the branch point of the transmit work, so
the two touched the same settings record, the same side panel, and the
same view. Both sides are kept wherever they met:

- `Settings` carries the transmit fields, the station, the contact
  options, and the remembered scope state together, and the TOML store
  writes and reads all of them.
- The side panel's action row opens the scope above Clear and Resync,
  under the transmit and contact sections the other branch added.
- `view` draws the scope after the dialogs, so it is still shown after
  everything it can be opened from.
- `AudioState` keeps the playback half beside the capture tap.

The two progress notes are reconciled rather than picked: steps 1
through 5 of the implementation order are now done, and what is left is
the aligned save, the history log, and the rig frequency readout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@kb10uy
kb10uy merged commit 06920fe into master Sep 13, 2026
7 checks passed
@kb10uy
kb10uy deleted the rtty branch September 13, 2026 05:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant