Commit Graph
4 Commits
Author SHA1 Message Date
rouggy d4d22eb4b2 fix(tci): the mode was never the rule — ask the radio instead
Transmit over TCI is confirmed on a SunSDR: 80 W out of a 1 kHz tone at
70% of full scale into 80% drive, every request answered.

Which makes the earlier rule wrong. 'Digital modes only' came from a real
observation — SSB silent four times over, DIGU answering at once — but the
mode was a coincidence. ExpertSDR3 has a transmit audio SOURCE, microphone
or TCI, kept per mode, and it was on the microphone in SSB. Refusing SSB
would have blocked the one thing a voice keyer exists for.

So nothing is refused on the strength of the mode. The radio declares what
it wants by asking for audio, 47 times a second when it wants any: key,
wait for one request, and stop within two tenths of a second if none
comes — naming the setting to change rather than theorising about it.

That is better on three counts. It works in SSB when the source is set
right, it cannot be wrong about a mode nobody thought to test (AM, FM,
RTTY), and a misconfiguration costs a quarter-second of carrier instead of
five seconds.
2026-08-25 23:51:28 +02:00
rouggy 592dd08835 fix(tci): send the tone near full scale, and log both levels
The transmit path works: six passes, the radio asked 231 times and was
answered 231 times, none missed, and the tone was there on the panadapter.

What was missing was power on the meter, and the cause was the level. The
tone went out at a quarter of full scale, out of caution, into a radio set
to 15% drive — enough to draw a clean signal and not enough to move a
needle. In a digital mode the radio expects a line level it can drive to
full output; the POWER is its own drive control, so sending quietly only
wastes the range. Now 0.7, short of 1.0 to leave room for the peaks.

The start line carries both numbers — ours and the radio's drive — because
a quiet transmission has two possible causes and one line should settle
which, rather than an evening of guessing. Reading 'drive' off the radio
is the only reason it is parsed at all.
2026-08-25 23:35:08 +02:00
rouggy c6294d9eb3 feat(tci): answer the radio's requests instead of pushing audio at it
Three transmissions on a real SunSDR settled how the transmit side works,
and none of it was guessable from the documentation.

The radio asks for audio only when the transmission is the CLIENT'S: with
the operator keying the microphone it sent 282 receive frames and nothing
else. And it asks only in a DIGITAL mode — keyed from here in SSB it stayed
silent four times over, and answered in DIGU immediately. In SSB the
modulator is wired to the microphone, which is also the honest answer to
'why can I hear myself but not the tone'.

The chrono turns out to be a REQUEST, not a clock. It carries no payload —
the message itself is the ask — and it names the size it wants in the
header: 2048 samples, two channels interleaved, 47 times a second, which
is 1024 pairs at 48 kHz, exactly real time.

So audio goes out in answer to a request and never on a timer of our own.
The timer was the first attempt and the radio ignored all 234 frames of
it. Answering also hands the pacing to the radio: no drift, no buffer to
tune, and the size taken from what it asked for rather than from what we
assumed. The sine keeps its phase across frames, since one restarted every
frame is a click 47 times a second.

A pass in SSB is now refused rather than attempted. It keys the
transmitter, produces nothing and teaches nobody anything — and it is
still a transmission.

The feed mechanism is the one the voice keyer will use: WAV samples in
place of the sine, everything else unchanged.
2026-08-25 23:27:26 +02:00
rouggy 848ce68ec5 feat(tci): key the radio ourselves and push a tone, to see what it wants
A first real transmission settled one question and raised a better one.
With the receive stream open and six seconds of transmit, the radio sent
282 frames of receive audio and NOTHING else: no chrono, no transmit
audio. So the chrono the documentation describes is not offered to a
client that merely happens to be connected while the operator keys the
microphone, and waiting for it to appear is waiting for nothing.

The reading that fits is that the radio asks for audio when the
transmission is the CLIENT'S and takes the microphone when it is the
operator's — which makes the experiment obvious. Key it from here, push a
1 kHz tone, and watch. Chrono frames appearing gives their size and
cadence by measurement instead of by guesswork; no chrono but a tone on
the meter is just as useful, because then the pacing is optional and the
voice keyer can push frames at the rate the stream already runs at.

A tone rather than silence so the answer shows on the power meter and not
only in the log.

It transmits, so: an explicit button inside a warning box, five seconds,
capped at ten, and every path out unkeys — including the panic that has
not happened yet and a socket that dies mid-tone. A transmitter left keyed
by a defect is the one fault here that would reach somebody else's band.

Writes are now serialised too. send() held the lock only long enough to
read the connection, which was enough while every command came from the
poll loop; a stream of audio frames from a second goroutine is not, and
gorilla panics on a concurrent write rather than failing quietly.
2026-08-25 23:18:11 +02:00